Compare commits

..
1 Commits
Author SHA1 Message Date
Meier Lukas 8889537ff8 wip: libsql migration 2025-12-19 22:11:35 +01:00
74 changed files with 1845 additions and 2715 deletions

No files matched your search

-52
View File
@@ -1,55 +1,3 @@
## [1.49.0](https://github.com/homarr-labs/homarr/compare/v1.48.0...v1.49.0) (2026-01-02)
### Features
* unraid integration ([#4439](https://github.com/homarr-labs/homarr/issues/4439)) ([9b0dd6d](https://github.com/homarr-labs/homarr/commit/9b0dd6d6cebda4a541c8859506d89e9e9f782fa4))
### Bug Fixes
* add orphaned status for containers with invalid stats ([#4441](https://github.com/homarr-labs/homarr/issues/4441)) ([f04f537](https://github.com/homarr-labs/homarr/commit/f04f53759b6890d5d2dd683a9bf124d8ff12780b))
* **deps:** update dependency @xterm/addon-fit to v0.11.0 ([#4756](https://github.com/homarr-labs/homarr/issues/4756)) ([1c727e5](https://github.com/homarr-labs/homarr/commit/1c727e59af4041e5e41da854633ccd3c030c1d18))
* **deps:** update dependency @xterm/xterm to v6 ([#4757](https://github.com/homarr-labs/homarr/issues/4757)) ([fad478c](https://github.com/homarr-labs/homarr/commit/fad478c549cdbc616f921d9b444bff48b59e77a5))
* **deps:** update dependency isomorphic-dompurify to ^2.35.0 ([#4779](https://github.com/homarr-labs/homarr/issues/4779)) ([236fa35](https://github.com/homarr-labs/homarr/commit/236fa35f7e0943be33bba153e7030ce4e3bb204e))
* **deps:** update dependency jotai to ^2.16.1 ([#4772](https://github.com/homarr-labs/homarr/issues/4772)) ([d002700](https://github.com/homarr-labs/homarr/commit/d00270046a041a10c1c540cfa79c1eee477f3303))
* **deps:** update dependency ldapts to v8.0.31 ([#4755](https://github.com/homarr-labs/homarr/issues/4755)) ([8b09693](https://github.com/homarr-labs/homarr/commit/8b096939e4e1da8e98791a8e05f108a136fe2810))
* **deps:** update dependency ldapts to v8.0.32 ([#4762](https://github.com/homarr-labs/homarr/issues/4762)) ([49afb82](https://github.com/homarr-labs/homarr/commit/49afb8244615f67af0586aaacf77a509231ac6dc))
* **deps:** update dependency ldapts to v8.0.34 ([#4765](https://github.com/homarr-labs/homarr/issues/4765)) ([809bf55](https://github.com/homarr-labs/homarr/commit/809bf559fe1b96df9a903dc790d138cad7368adb))
* **deps:** update dependency ldapts to v8.0.35 ([#4768](https://github.com/homarr-labs/homarr/issues/4768)) ([e574dd7](https://github.com/homarr-labs/homarr/commit/e574dd70f1421e9254a289a0f02fd0b4d43e26ad))
* **deps:** update dependency typescript-eslint to ^8.50.1 ([#4758](https://github.com/homarr-labs/homarr/issues/4758)) ([adaa803](https://github.com/homarr-labs/homarr/commit/adaa8039dd545109246ccd9fa15570739f755f62))
* **deps:** update nextjs monorepo to v16.1.1 ([#4759](https://github.com/homarr-labs/homarr/issues/4759)) ([7712ab4](https://github.com/homarr-labs/homarr/commit/7712ab410b5094e5d84df75461e07efdfccc285b))
* **deps:** update tanstack-query monorepo ([#4782](https://github.com/homarr-labs/homarr/issues/4782)) ([1e5d38f](https://github.com/homarr-labs/homarr/commit/1e5d38fd3ca4c68fa4c61b02757a3cff39839fdb))
* **deps:** update testcontainers-node monorepo to ^11.11.0 ([#4781](https://github.com/homarr-labs/homarr/issues/4781)) ([e40a116](https://github.com/homarr-labs/homarr/commit/e40a11608cb21a75e6910028abf97e14b5275f56))
* **deps:** update turbo monorepo to ^2.7.2 ([#4764](https://github.com/homarr-labs/homarr/issues/4764)) ([9191430](https://github.com/homarr-labs/homarr/commit/919143022969350f6ee7be2f53be2fe09a89bba3))
## [1.48.0](https://github.com/homarr-labs/homarr/compare/v1.47.0...v1.48.0) (2025-12-26)
### Features
* enable dns caching by default ([#4706](https://github.com/homarr-labs/homarr/issues/4706)) ([fbb5298](https://github.com/homarr-labs/homarr/commit/fbb5298cb05b12eb2f19c121a1bc103caeb3e576))
* **http:** add proxy support ([#4721](https://github.com/homarr-labs/homarr/issues/4721)) ([5a57115](https://github.com/homarr-labs/homarr/commit/5a57115ca073aea1c75a000fda6a23648c5488e4))
### Bug Fixes
* **deps:** update dependency @t3-oss/env-nextjs to ^0.13.10 ([#4710](https://github.com/homarr-labs/homarr/issues/4710)) ([689523a](https://github.com/homarr-labs/homarr/commit/689523a194ab48bdc072b1cdf65f07ade6cbc584))
* **deps:** update dependency ldapts to v8.0.24 ([#4711](https://github.com/homarr-labs/homarr/issues/4711)) ([648f7e2](https://github.com/homarr-labs/homarr/commit/648f7e2c7743af81f4bced36b7e1e80c21e851e5))
* **deps:** update dependency ldapts to v8.0.25 ([#4724](https://github.com/homarr-labs/homarr/issues/4724)) ([ef12b2b](https://github.com/homarr-labs/homarr/commit/ef12b2b5c2e1131138937fb6da61ce4c167fe0eb))
* **deps:** update dependency ldapts to v8.0.27 ([#4737](https://github.com/homarr-labs/homarr/issues/4737)) ([8891ac4](https://github.com/homarr-labs/homarr/commit/8891ac42399e47fda6efc657b8ff2fe58aebca9e))
* **deps:** update dependency ldapts to v8.0.28 ([#4743](https://github.com/homarr-labs/homarr/issues/4743)) ([75c5453](https://github.com/homarr-labs/homarr/commit/75c545344aae39550917331458c01a894b6a942f))
* **deps:** update dependency ldapts to v8.0.29 ([#4747](https://github.com/homarr-labs/homarr/issues/4747)) ([5dd7400](https://github.com/homarr-labs/homarr/commit/5dd74006843ae72f63e631b14f0fd50c911d610b))
* **deps:** update dependency ldapts to v8.0.30 ([#4749](https://github.com/homarr-labs/homarr/issues/4749)) ([96c2c0d](https://github.com/homarr-labs/homarr/commit/96c2c0d604737c64722fa21bf79965c806bf4da3))
* **deps:** update dependency mysql2 to v3.16.0 ([#4731](https://github.com/homarr-labs/homarr/issues/4731)) ([f34bdce](https://github.com/homarr-labs/homarr/commit/f34bdce6446d4e62b54b96f63e91cfdddd51e901))
* **deps:** update dependency next-intl to v4.6.1 ([#4730](https://github.com/homarr-labs/homarr/issues/4730)) ([f4d782a](https://github.com/homarr-labs/homarr/commit/f4d782abefb78e085ca3fc66034c0ca54fb21120))
* **deps:** update dependency sass to ^1.97.0 ([#4726](https://github.com/homarr-labs/homarr/issues/4726)) ([88baad8](https://github.com/homarr-labs/homarr/commit/88baad8143f4ca830e50644d6bd1d1980ef1da82))
* **deps:** update dependency sass to ^1.97.1 ([#4746](https://github.com/homarr-labs/homarr/issues/4746)) ([6b774f4](https://github.com/homarr-labs/homarr/commit/6b774f47d0a8bf731e7e1837a9d47e0dc53a1ff0))
* **deps:** update dependency typescript-eslint to ^8.50.0 ([#4720](https://github.com/homarr-labs/homarr/issues/4720)) ([2dc8fea](https://github.com/homarr-labs/homarr/commit/2dc8feae7ea6174a1d826d8f0151ddc2c04c6aa6))
* **deps:** update dependency zod to ^4.2.0 ([#4712](https://github.com/homarr-labs/homarr/issues/4712)) ([6e39524](https://github.com/homarr-labs/homarr/commit/6e39524f9d28d201c13738212d238726e1685832))
* **deps:** update dependency zod to ^4.2.1 ([#4722](https://github.com/homarr-labs/homarr/issues/4722)) ([68dc684](https://github.com/homarr-labs/homarr/commit/68dc6844fdee53aca55b44dd7d19bf9d3a35c057))
* **deps:** update nextjs monorepo to v16.1.0 ([#4739](https://github.com/homarr-labs/homarr/issues/4739)) ([99f2842](https://github.com/homarr-labs/homarr/commit/99f2842a5a102056c76ddc68cb5083791f324c3c))
* **deps:** update tiptap monorepo to v3.14.0 ([#4744](https://github.com/homarr-labs/homarr/issues/4744)) ([adbe707](https://github.com/homarr-labs/homarr/commit/adbe707c73f462771c3d21cbbcc21e8245eea6f3))
* **deps:** update trpc monorepo to ^11.8.1 ([#4750](https://github.com/homarr-labs/homarr/issues/4750)) ([93bf9ab](https://github.com/homarr-labs/homarr/commit/93bf9ab84b5387eefb8cc0b03057a4ccf0b13aa9))
* **deps:** update turbo monorepo to ^2.7.0 ([#4745](https://github.com/homarr-labs/homarr/issues/4745)) ([5d659a3](https://github.com/homarr-labs/homarr/commit/5d659a3ebbf79266158cb57236de90e7883e7a4e))
* **deps:** update turbo monorepo to ^2.7.1 ([#4751](https://github.com/homarr-labs/homarr/issues/4751)) ([d8db016](https://github.com/homarr-labs/homarr/commit/d8db0169a142c2ec302806fe946cb17431ccae6b))
## [1.47.0](https://github.com/homarr-labs/homarr/compare/v1.46.0...v1.47.0) (2025-12-19) ## [1.47.0](https://github.com/homarr-labs/homarr/compare/v1.46.0...v1.47.0) (2025-12-19)
### Features ### Features
-1
View File
@@ -45,7 +45,6 @@ COPY --from=builder /app/apps/nextjs/package.json .
COPY --from=builder /app/apps/tasks/tasks.cjs ./apps/tasks/tasks.cjs COPY --from=builder /app/apps/tasks/tasks.cjs ./apps/tasks/tasks.cjs
COPY --from=builder /app/apps/websocket/wssServer.cjs ./apps/websocket/wssServer.cjs COPY --from=builder /app/apps/websocket/wssServer.cjs ./apps/websocket/wssServer.cjs
COPY --from=builder /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node
COPY --from=builder /app/packages/db/migrations ./db/migrations COPY --from=builder /app/packages/db/migrations ./db/migrations
+13 -13
View File
@@ -56,16 +56,16 @@
"@mantine/tiptap": "^8.3.10", "@mantine/tiptap": "^8.3.10",
"@million/lint": "1.0.14", "@million/lint": "1.0.14",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"@tanstack/react-query": "^5.90.14", "@tanstack/react-query": "^5.90.12",
"@tanstack/react-query-devtools": "^5.91.2", "@tanstack/react-query-devtools": "^5.91.1",
"@tanstack/react-query-next-experimental": "^5.91.0", "@tanstack/react-query-next-experimental": "^5.91.0",
"@trpc/client": "^11.8.1", "@trpc/client": "^11.8.0",
"@trpc/next": "^11.8.1", "@trpc/next": "^11.8.0",
"@trpc/react-query": "^11.8.1", "@trpc/react-query": "^11.8.0",
"@trpc/server": "^11.8.1", "@trpc/server": "^11.8.0",
"@xterm/addon-canvas": "^0.7.0", "@xterm/addon-canvas": "^0.7.0",
"@xterm/addon-fit": "0.11.0", "@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "^6.0.0", "@xterm/xterm": "^5.5.0",
"babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-compiler": "^1.0.0",
"chroma-js": "^3.2.0", "chroma-js": "^3.2.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -73,21 +73,21 @@
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"flag-icons": "^7.5.0", "flag-icons": "^7.5.0",
"glob": "^13.0.0", "glob": "^13.0.0",
"isomorphic-dompurify": "^2.35.0", "isomorphic-dompurify": "^2.34.0",
"jotai": "^2.16.1", "jotai": "^2.16.0",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "16.1.1", "next": "16.0.10",
"postcss-preset-mantine": "^1.18.0", "postcss-preset-mantine": "^1.18.0",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"react-error-boundary": "^6.0.0", "react-error-boundary": "^6.0.0",
"react-simple-code-editor": "^0.14.1", "react-simple-code-editor": "^0.14.1",
"sass": "^1.97.1", "sass": "^1.96.0",
"superjson": "2.2.6", "superjson": "2.2.6",
"swagger-ui-react": "^5.31.0", "swagger-ui-react": "^5.31.0",
"use-deep-compare-effect": "^1.8.1", "use-deep-compare-effect": "^1.8.1",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
@@ -76,7 +76,6 @@ const createColumns = (
accessorKey: "ports", accessorKey: "ports",
header: t("docker.field.ports.label"), header: t("docker.field.ports.label"),
Cell({ cell }) { Cell({ cell }) {
if (!cell.row.original.ports.length) return null;
return ( return (
<OverflowBadge overflowCount={1} data={cell.row.original.ports.map((port) => port.PrivatePort.toString())} /> <OverflowBadge overflowCount={1} data={cell.row.original.ports.map((port) => port.PrivatePort.toString())} />
); );
+1 -1
View File
@@ -49,7 +49,7 @@
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^24.10.4", "@types/node": "^24.10.4",
"dotenv-cli": "^11.0.0", "dotenv-cli": "^11.0.0",
"esbuild": "^0.27.2", "esbuild": "^0.27.1",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"prettier": "^3.7.4", "prettier": "^3.7.4",
"tsx": "4.20.4", "tsx": "4.20.4",
+1 -1
View File
@@ -34,7 +34,7 @@
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"esbuild": "^0.27.2", "esbuild": "^0.27.1",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"prettier": "^3.7.4", "prettier": "^3.7.4",
"typescript": "^5.9.3" "typescript": "^5.9.3"
+2 -2
View File
@@ -4,10 +4,10 @@
"dependencies": { "dependencies": {
"better-sqlite3": "^12.5.0" "better-sqlite3": "^12.5.0"
}, },
"packageManager": "pnpm@10.26.2", "packageManager": "pnpm@10.25.0",
"engines": { "engines": {
"node": ">=24.12.0", "node": ">=24.12.0",
"pnpm": ">=10.26.2" "pnpm": ">=10.25.0"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
-7
View File
@@ -314,13 +314,6 @@
<br/> <br/>
<p align="center">Unifi<br/>Controller</p> <p align="center">Unifi<br/>Controller</p>
</a> </a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/unraid" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/unraid.svg" alt="Unraid" width="90" height="90" />
<br/>
<p align="center">Unraid</p>
</a>
</td></tr> </td></tr>
</tbody> </tbody>
</table> </table>
+16 -17
View File
@@ -1,6 +1,6 @@
{ {
"name": "homarr", "name": "homarr",
"version": "1.49.0", "version": "1.47.0",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "cross-env CI=true turbo build", "build": "cross-env CI=true turbo build",
@@ -42,33 +42,32 @@
"@semantic-release/github": "^12.0.2", "@semantic-release/github": "^12.0.2",
"@semantic-release/npm": "^13.1.3", "@semantic-release/npm": "^13.1.3",
"@semantic-release/release-notes-generator": "^14.1.0", "@semantic-release/release-notes-generator": "^14.1.0",
"@testcontainers/redis": "^11.11.0", "@testcontainers/redis": "^11.10.0",
"@turbo/gen": "^2.7.2", "@turbo/gen": "^2.6.3",
"@vitejs/plugin-react": "^5.1.2", "@vitejs/plugin-react": "^5.1.2",
"@vitest/coverage-v8": "^4.0.16", "@vitest/coverage-v8": "^4.0.15",
"@vitest/ui": "^4.0.16", "@vitest/ui": "^4.0.15",
"conventional-changelog-conventionalcommits": "^9.1.0", "conventional-changelog-conventionalcommits": "^9.1.0",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"jsdom": "^27.4.0", "jsdom": "^27.3.0",
"json5": "^2.2.3", "json5": "^2.2.3",
"prettier": "^3.7.4", "prettier": "^3.7.4",
"semantic-release": "^25.0.2", "semantic-release": "^25.0.2",
"testcontainers": "^11.11.0", "testcontainers": "^11.10.0",
"turbo": "^2.7.2", "turbo": "^2.6.3",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite-tsconfig-paths": "^6.0.3", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^4.0.16" "vitest": "^4.0.15"
}, },
"packageManager": "pnpm@10.26.2", "packageManager": "pnpm@10.25.0",
"engines": { "engines": {
"node": ">=24.12.0", "node": ">=24.12.0",
"pnpm": ">=10.26.2" "pnpm": ">=10.25.0"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"@tree-sitter-grammars/tree-sitter-yaml", "@tree-sitter-grammars/tree-sitter-yaml",
"bcrypt", "bcrypt",
"better-sqlite3",
"cpu-features", "cpu-features",
"esbuild", "esbuild",
"sharp", "sharp",
@@ -82,20 +81,20 @@
"axios@>=1.0.0 <1.8.2": ">=1.13.2", "axios@>=1.0.0 <1.8.2": ">=1.13.2",
"brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1", "brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1",
"brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1", "brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1",
"esbuild@<=0.24.2": ">=0.27.2", "esbuild@<=0.24.2": ">=0.27.1",
"form-data@>=4.0.0 <4.0.4": ">=4.0.5", "form-data@>=4.0.0 <4.0.4": ">=4.0.5",
"hono@<4.6.5": ">=4.11.3", "hono@<4.6.5": ">=4.11.0",
"linkifyjs@<4.3.2": ">=4.3.2", "linkifyjs@<4.3.2": ">=4.3.2",
"nanoid@>=4.0.0 <5.0.9": ">=5.1.6", "nanoid@>=4.0.0 <5.0.9": ">=5.1.6",
"prismjs@<1.30.0": ">=1.30.0", "prismjs@<1.30.0": ">=1.30.0",
"proxmox-api>undici": "7.16.0", "proxmox-api>undici": "7.16.0",
"react-is": "^19.2.3", "react-is": "^19.2.3",
"rollup@>=4.0.0 <4.22.4": ">=4.54.0", "rollup@>=4.0.0 <4.22.4": ">=4.53.3",
"sha.js@<=2.4.11": ">=2.4.12", "sha.js@<=2.4.11": ">=2.4.12",
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.1", "tar-fs@>=3.0.0 <3.0.9": ">=3.1.1",
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.1", "tar-fs@>=2.0.0 <2.1.3": ">=3.1.1",
"tmp@<=0.2.3": ">=0.2.5", "tmp@<=0.2.3": ">=0.2.5",
"vite@>=5.0.0 <=5.4.18": ">=7.3.0" "vite@>=5.0.0 <=5.4.18": ">=7.2.7"
}, },
"patchedDependencies": { "patchedDependencies": {
"@types/node-unifi": "patches/@types__node-unifi.patch", "@types/node-unifi": "patches/@types__node-unifi.patch",
+7 -7
View File
@@ -41,18 +41,18 @@
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@kubernetes/client-node": "^1.4.0", "@kubernetes/client-node": "^1.4.0",
"@tanstack/react-query": "^5.90.14", "@tanstack/react-query": "^5.90.12",
"@trpc/client": "^11.8.1", "@trpc/client": "^11.8.0",
"@trpc/react-query": "^11.8.1", "@trpc/react-query": "^11.8.0",
"@trpc/server": "^11.8.1", "@trpc/server": "^11.8.0",
"@trpc/tanstack-react-query": "^11.8.1", "@trpc/tanstack-react-query": "^11.8.0",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"next": "16.1.1", "next": "16.0.10",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"superjson": "2.2.6", "superjson": "2.2.6",
"trpc-to-openapi": "^3.1.0", "trpc-to-openapi": "^3.1.0",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
+10 -8
View File
@@ -5,7 +5,7 @@ import { createId } from "@homarr/common";
import { asc, eq, inArray, like } from "@homarr/db"; import { asc, eq, inArray, like } from "@homarr/db";
import { apps } from "@homarr/db/schema"; import { apps } from "@homarr/db/schema";
import { selectAppSchema } from "@homarr/db/validationSchemas"; import { selectAppSchema } from "@homarr/db/validationSchemas";
import { getIconForName } from "@homarr/icons"; import { getIconForNameAsync } from "@homarr/icons";
import { appCreateManySchema, appEditSchema, appManageSchema } from "@homarr/validation/app"; import { appCreateManySchema, appEditSchema, appManageSchema } from "@homarr/validation/app";
import { byIdSchema, paginatedSchema } from "@homarr/validation/common"; import { byIdSchema, paginatedSchema } from "@homarr/validation/common";
@@ -141,13 +141,15 @@ export const appRouter = createTRPCRouter({
.output(z.void()) .output(z.void())
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
await ctx.db.insert(apps).values( await ctx.db.insert(apps).values(
input.map((app) => ({ await Promise.all(
id: createId(), input.map(async (app) => ({
name: app.name, id: createId(),
description: app.description, name: app.name,
iconUrl: app.iconUrl ?? getIconForName(ctx.db, app.name).sync()?.url ?? defaultIcon, description: app.description,
href: app.href, iconUrl: app.iconUrl ?? (await getIconForNameAsync(ctx.db, app.name))?.url ?? defaultIcon,
})), href: app.href,
})),
),
); );
}), }),
update: permissionRequiredProcedure update: permissionRequiredProcedure
-287
View File
@@ -477,47 +477,6 @@ export const boardRouter = createTRPCRouter({
} }
}); });
}, },
handleSync(db) {
db.transaction((transaction) => {
transaction
.insert(boards)
.values({
...boardProps,
id: newBoardId,
name: input.name,
creatorId: ctx.session.user.id,
})
.run();
if (layoutsToInsert.length > 0) {
transaction.insert(layouts).values(layoutsToInsert).run();
}
if (sectionsToInsert.length > 0) {
transaction.insert(sections).values(sectionsToInsert).run();
}
if (sectionLayoutsToInsert.length > 0) {
transaction.insert(sectionLayouts).values(sectionLayoutsToInsert).run();
}
if (sectionCollapseStatesToInsert.length > 0) {
transaction.insert(sectionCollapseStates).values(sectionCollapseStatesToInsert).run();
}
if (itemsToInsert.length > 0) {
transaction.insert(items).values(itemsToInsert).run();
}
if (itemLayoutsToInsert.length > 0) {
transaction.insert(itemLayouts).values(itemLayoutsToInsert).run();
}
if (itemIntegrationsToInsert.length > 0) {
transaction.insert(integrationItems).values(itemIntegrationsToInsert).run();
}
});
},
}); });
}), }),
renameBoard: protectedProcedure.input(boardRenameSchema).mutation(async ({ ctx, input }) => { renameBoard: protectedProcedure.input(boardRenameSchema).mutation(async ({ ctx, input }) => {
@@ -954,216 +913,6 @@ export const boardRouter = createTRPCRouter({
} }
}); });
}, },
handleSync(db) {
db.transaction((transaction) => {
const addedSections = filterAddedItems(input.sections, dbBoard.sections);
if (addedSections.length > 0) {
transaction
.insert(sections)
.values(
addedSections.map((section) => ({
id: section.id,
kind: section.kind,
yOffset: section.kind !== "dynamic" ? section.yOffset : null,
xOffset: section.kind === "dynamic" ? null : 0,
options: section.kind === "dynamic" ? superjson.stringify(section.options) : emptySuperJSON,
name: "name" in section ? section.name : null,
boardId: dbBoard.id,
})),
)
.run();
if (addedSections.some((section) => section.kind === "dynamic")) {
transaction
.insert(sectionLayouts)
.values(
addedSections
.filter((section) => section.kind === "dynamic")
.flatMap((section) =>
section.layouts.map(
(sectionLayout): InferInsertModel<typeof sectionLayouts> => ({
layoutId: sectionLayout.layoutId,
sectionId: section.id,
parentSectionId: sectionLayout.parentSectionId,
height: sectionLayout.height,
width: sectionLayout.width,
xOffset: sectionLayout.xOffset,
yOffset: sectionLayout.yOffset,
}),
),
),
)
.run();
}
}
const addedItems = filterAddedItems(input.items, dbBoard.items);
if (addedItems.length > 0) {
transaction
.insert(items)
.values(
addedItems.map((item) => ({
id: item.id,
kind: item.kind,
options: superjson.stringify(item.options),
advancedOptions: superjson.stringify(item.advancedOptions),
boardId: dbBoard.id,
})),
)
.run();
transaction
.insert(itemLayouts)
.values(
addedItems.flatMap((item) =>
item.layouts.map(
(layoutSection): InferInsertModel<typeof itemLayouts> => ({
layoutId: layoutSection.layoutId,
sectionId: layoutSection.sectionId,
itemId: item.id,
height: layoutSection.height,
width: layoutSection.width,
xOffset: layoutSection.xOffset,
yOffset: layoutSection.yOffset,
}),
),
),
)
.run();
}
const inputIntegrationRelations = input.items.flatMap(({ integrationIds, id: itemId }) =>
integrationIds.map((integrationId) => ({
integrationId,
itemId,
})),
);
const dbIntegrationRelations = dbBoard.items.flatMap(({ integrationIds, id: itemId }) =>
integrationIds.map((integrationId) => ({
integrationId,
itemId,
})),
);
const addedIntegrationRelations = inputIntegrationRelations.filter(
(inputRelation) =>
!dbIntegrationRelations.some(
(dbRelation) =>
dbRelation.itemId === inputRelation.itemId &&
dbRelation.integrationId === inputRelation.integrationId,
),
);
if (addedIntegrationRelations.length > 0) {
transaction
.insert(integrationItems)
.values(
addedIntegrationRelations.map((relation) => ({
itemId: relation.itemId,
integrationId: relation.integrationId,
})),
)
.run();
}
const updatedItems = filterUpdatedItems(input.items, dbBoard.items);
for (const item of updatedItems) {
transaction
.update(items)
.set({
kind: item.kind,
options: superjson.stringify(item.options),
advancedOptions: superjson.stringify(item.advancedOptions),
})
.where(eq(items.id, item.id))
.run();
for (const itemSectionLayout of item.layouts) {
transaction
.update(itemLayouts)
.set({
height: itemSectionLayout.height,
width: itemSectionLayout.width,
xOffset: itemSectionLayout.xOffset,
yOffset: itemSectionLayout.yOffset,
sectionId: itemSectionLayout.sectionId,
})
.where(and(eq(itemLayouts.itemId, item.id), eq(itemLayouts.layoutId, itemSectionLayout.layoutId)))
.run();
}
}
const updatedSections = filterUpdatedItems(input.sections, dbBoard.sections);
for (const section of updatedSections) {
const prev = dbBoard.sections.find((dbSection) => dbSection.id === section.id);
transaction
.update(sections)
.set({
yOffset: prev?.kind !== "dynamic" && "yOffset" in section ? section.yOffset : null,
xOffset: prev?.kind !== "dynamic" && "yOffset" in section ? 0 : null,
options: section.kind === "dynamic" ? superjson.stringify(section.options) : emptySuperJSON,
name: prev?.kind === "category" && "name" in section ? section.name : null,
})
.where(eq(sections.id, section.id))
.run();
if (section.kind !== "dynamic") continue;
for (const sectionLayout of section.layouts) {
transaction
.update(sectionLayouts)
.set({
height: sectionLayout.height,
width: sectionLayout.width,
xOffset: sectionLayout.xOffset,
yOffset: sectionLayout.yOffset,
parentSectionId: sectionLayout.parentSectionId,
})
.where(
and(eq(sectionLayouts.sectionId, section.id), eq(sectionLayouts.layoutId, sectionLayout.layoutId)),
)
.run();
}
}
const removedIntegrationRelations = dbIntegrationRelations.filter(
(dbRelation) =>
!inputIntegrationRelations.some(
(inputRelation) =>
dbRelation.itemId === inputRelation.itemId &&
dbRelation.integrationId === inputRelation.integrationId,
),
);
for (const relation of removedIntegrationRelations) {
transaction
.delete(integrationItems)
.where(
and(
eq(integrationItems.itemId, relation.itemId),
eq(integrationItems.integrationId, relation.integrationId),
),
)
.run();
}
const removedItems = filterRemovedItems(input.items, dbBoard.items);
const itemIds = removedItems.map((item) => item.id);
if (itemIds.length > 0) {
transaction.delete(items).where(inArray(items.id, itemIds)).run();
}
const removedSections = filterRemovedItems(input.sections, dbBoard.sections);
const sectionIds = removedSections.map((section) => section.id);
if (sectionIds.length > 0) {
transaction.delete(sections).where(inArray(sections.id, sectionIds)).run();
}
});
},
}); });
}), }),
getBoardPermissions: protectedProcedure.input(byIdSchema).query(async ({ input, ctx }) => { getBoardPermissions: protectedProcedure.input(byIdSchema).query(async ({ input, ctx }) => {
@@ -1256,24 +1005,6 @@ export const boardRouter = createTRPCRouter({
); );
}); });
}, },
handleSync(db) {
db.transaction((transaction) => {
transaction.delete(boardUserPermissions).where(eq(boardUserPermissions.boardId, input.entityId)).run();
if (input.permissions.length === 0) {
return;
}
transaction
.insert(boardUserPermissions)
.values(
input.permissions.map((permission) => ({
userId: permission.principalId,
permission: permission.permission,
boardId: input.entityId,
})),
)
.run();
});
},
}); });
}), }),
saveGroupBoardPermissions: protectedProcedure.input(boardSavePermissionsSchema).mutation(async ({ input, ctx }) => { saveGroupBoardPermissions: protectedProcedure.input(boardSavePermissionsSchema).mutation(async ({ input, ctx }) => {
@@ -1297,24 +1028,6 @@ export const boardRouter = createTRPCRouter({
); );
}); });
}, },
handleSync(db) {
db.transaction((transaction) => {
transaction.delete(boardGroupPermissions).where(eq(boardGroupPermissions.boardId, input.entityId)).run();
if (input.permissions.length === 0) {
return;
}
transaction
.insert(boardGroupPermissions)
.values(
input.permissions.map((permission) => ({
groupId: permission.principalId,
permission: permission.permission,
boardId: input.entityId,
})),
)
.run();
});
},
}); });
}), }),
importOldmarrConfig: permissionRequiredProcedure importOldmarrConfig: permissionRequiredProcedure
@@ -91,7 +91,7 @@ export const certificateRouter = createTRPCRouter({
hostname: input.hostname, hostname: input.hostname,
thumbprint: input.thumbprint, thumbprint: input.thumbprint,
}); });
const dbResult = await ctx.db await ctx.db
.delete(trustedCertificateHostnames) .delete(trustedCertificateHostnames)
.where( .where(
and( and(
@@ -103,7 +103,6 @@ export const certificateRouter = createTRPCRouter({
logger.info("Removed trusted hostname", { logger.info("Removed trusted hostname", {
hostname: input.hostname, hostname: input.hostname,
thumbprint: input.thumbprint, thumbprint: input.thumbprint,
count: dbResult.changes,
}); });
}), }),
removeCertificate: permissionRequiredProcedure removeCertificate: permissionRequiredProcedure
-7
View File
@@ -260,13 +260,6 @@ export const groupRouter = createTRPCRouter({
} }
}); });
}, },
handleSync: (db) => {
db.transaction((trx) => {
for (const { id, position } of positions) {
trx.update(groups).set({ position }).where(eq(groups.id, id)).run();
}
});
},
}); });
}), }),
savePermissions: permissionRequiredProcedure savePermissions: permissionRequiredProcedure
@@ -541,27 +541,6 @@ export const integrationRouter = createTRPCRouter({
); );
}); });
}, },
handleSync(db) {
db.transaction((transaction) => {
transaction
.delete(integrationUserPermissions)
.where(eq(integrationUserPermissions.integrationId, input.entityId))
.run();
if (input.permissions.length === 0) {
return;
}
transaction
.insert(integrationUserPermissions)
.values(
input.permissions.map((permission) => ({
userId: permission.principalId,
permission: permission.permission,
integrationId: input.entityId,
})),
)
.run();
});
},
}); });
}), }),
saveGroupIntegrationPermissions: protectedProcedure saveGroupIntegrationPermissions: protectedProcedure
@@ -587,27 +566,6 @@ export const integrationRouter = createTRPCRouter({
); );
}); });
}, },
handleSync(db) {
db.transaction((transaction) => {
transaction
.delete(integrationGroupPermissions)
.where(eq(integrationGroupPermissions.integrationId, input.entityId))
.run();
if (input.permissions.length === 0) {
return;
}
transaction
.insert(integrationGroupPermissions)
.values(
input.permissions.map((permission) => ({
groupId: permission.principalId,
permission: permission.permission,
integrationId: input.entityId,
})),
)
.run();
});
},
}); });
}), }),
searchInIntegration: protectedProcedure searchInIntegration: protectedProcedure
+2 -8
View File
@@ -1,7 +1,6 @@
import { z } from "zod/v4"; import { z } from "zod/v4";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { createTRPCRouter, publicProcedure } from "../trpc"; import { createTRPCRouter, publicProcedure } from "../trpc";
@@ -37,12 +36,7 @@ export const locationRouter = createTRPCRouter({
.input(locationSearchCityInput) .input(locationSearchCityInput)
.output(locationSearchCityOutput) .output(locationSearchCityOutput)
.query(async ({ input }) => { .query(async ({ input }) => {
const res = await withTimeoutAsync(async (signal) => { const res = await fetchWithTimeoutAsync(`https://geocoding-api.open-meteo.com/v1/search?name=${input.query}`);
return await fetchWithTrustedCertificatesAsync(
`https://geocoding-api.open-meteo.com/v1/search?name=${input.query}`,
{ signal },
);
});
return (await res.json()) as z.infer<typeof locationSearchCityOutput>; return (await res.json()) as z.infer<typeof locationSearchCityOutput>;
}), }),
}); });
@@ -9,7 +9,7 @@ import { createTRPCRouter, publicProcedure } from "../../trpc";
export const healthMonitoringRouter = createTRPCRouter({ export const healthMonitoringRouter = createTRPCRouter({
getSystemHealthStatus: publicProcedure getSystemHealthStatus: publicProcedure
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "truenas", "unraid", "mock")) .concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "truenas", "mock"))
.query(async ({ ctx }) => { .query(async ({ ctx }) => {
return await Promise.all( return await Promise.all(
ctx.integrations.map(async (integration) => { ctx.integrations.map(async (integration) => {
@@ -26,7 +26,7 @@ export const healthMonitoringRouter = createTRPCRouter({
); );
}), }),
subscribeSystemHealthStatus: publicProcedure subscribeSystemHealthStatus: publicProcedure
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "truenas", "unraid", "mock")) .concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "truenas", "mock"))
.subscription(({ ctx }) => { .subscription(({ ctx }) => {
return observable<{ integrationId: string; healthInfo: SystemHealthMonitoring; timestamp: Date }>((emit) => { return observable<{ integrationId: string; healthInfo: SystemHealthMonitoring; timestamp: Date }>((emit) => {
const unsubscribes: (() => void)[] = []; const unsubscribes: (() => void)[] = [];
+3 -3
View File
@@ -32,12 +32,12 @@
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"cookies": "^0.9.1", "cookies": "^0.9.1",
"ldapts": "8.0.35", "ldapts": "8.0.23",
"next": "16.1.1", "next": "16.0.10",
"next-auth": "5.0.0-beta.30", "next-auth": "5.0.0-beta.30",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
+1 -1
View File
@@ -34,7 +34,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"esbuild": "^0.27.2", "esbuild": "^0.27.1",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }
+2 -2
View File
@@ -29,12 +29,12 @@
"@homarr/core": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0",
"@paralleldrive/cuid2": "^3.1.0", "@paralleldrive/cuid2": "^3.1.0",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"next": "16.1.1", "next": "16.0.10",
"octokit": "^5.0.5", "octokit": "^5.0.5",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"undici": "7.16.0", "undici": "7.16.0",
"zod": "^4.2.1", "zod": "^4.1.13",
"zod-validation-error": "^5.0.0" "zod-validation-error": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
+4 -5
View File
@@ -37,22 +37,21 @@
}, },
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@t3-oss/env-nextjs": "^0.13.10", "@libsql/client": "^0.15.15",
"better-sqlite3": "^12.5.0", "@t3-oss/env-nextjs": "^0.13.8",
"dns-caching": "^0.2.9", "dns-caching": "^0.2.9",
"drizzle-orm": "^0.45.1", "drizzle-orm": "^0.45.1",
"ioredis": "5.8.2", "ioredis": "5.8.2",
"mysql2": "3.16.0", "mysql2": "3.15.3",
"pg": "^8.16.3", "pg": "^8.16.3",
"superjson": "2.2.6", "superjson": "2.2.6",
"winston": "3.19.0", "winston": "3.19.0",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/better-sqlite3": "7.6.13",
"@types/pg": "^8.16.0", "@types/pg": "^8.16.0",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"typescript": "^5.9.3" "typescript": "^5.9.3"
@@ -1,10 +1,10 @@
import Database from "better-sqlite3"; import { createClient } from "@libsql/client";
import { drizzle as drizzleSqlite } from "drizzle-orm/better-sqlite3"; import { drizzle as drizzleSqlite } from "drizzle-orm/libsql";
import { dbEnv } from "../env"; import { dbEnv } from "../env";
import type { SharedDrizzleConfig } from "./shared"; import type { SharedDrizzleConfig } from "./shared";
export const createSqliteDb = <TSchema extends Record<string, unknown>>(config: SharedDrizzleConfig<TSchema>) => { export const createSqliteDb = <TSchema extends Record<string, unknown>>(config: SharedDrizzleConfig<TSchema>) => {
const connection = new Database(dbEnv.URL); const client = createClient({ url: `file:${dbEnv.URL}` });
return drizzleSqlite<TSchema>(connection, config); return drizzleSqlite<TSchema>(client, config);
}; };
+1 -1
View File
@@ -2,7 +2,7 @@ import { createBooleanSchema, createEnv } from "../env";
export const dnsEnv = createEnv({ export const dnsEnv = createEnv({
server: { server: {
ENABLE_DNS_CACHING: createBooleanSchema(true), ENABLE_DNS_CACHING: createBooleanSchema(false),
}, },
experimental__runtimeEnv: process.env, experimental__runtimeEnv: process.env,
}); });
@@ -1,5 +1,5 @@
import type { Dispatcher } from "undici"; import type { Dispatcher } from "undici";
import { EnvHttpProxyAgent } from "undici"; import { Agent } from "undici";
import type { ILogger } from "@homarr/core/infrastructure/logs"; import type { ILogger } from "@homarr/core/infrastructure/logs";
import { createLogger } from "@homarr/core/infrastructure/logs"; import { createLogger } from "@homarr/core/infrastructure/logs";
@@ -7,15 +7,16 @@ import { createLogger } from "@homarr/core/infrastructure/logs";
// The below import statement initializes dns-caching // The below import statement initializes dns-caching
import "@homarr/core/infrastructure/dns/init"; import "@homarr/core/infrastructure/dns/init";
interface HttpAgentOptions extends EnvHttpProxyAgent.Options { interface HttpAgentOptions extends Agent.Options {
logger?: ILogger; logger?: ILogger;
} }
export class UndiciHttpAgent extends EnvHttpProxyAgent { export class UndiciHttpAgent extends Agent {
private logger: ILogger; private logger: ILogger;
constructor(props?: HttpAgentOptions) { constructor(props?: HttpAgentOptions) {
super(props); super(props);
this.logger = props?.logger ?? createLogger({ module: "httpAgent" }); this.logger = props?.logger ?? createLogger({ module: "httpAgent" });
} }
@@ -42,13 +42,12 @@ export const createCertificateAgentAsync = async (override?: {
}; };
export const createHttpsAgentAsync = async (override?: Pick<AgentOptions, "ca" | "checkServerIdentity">) => { export const createHttpsAgentAsync = async (override?: Pick<AgentOptions, "ca" | "checkServerIdentity">) => {
return new HttpsAgent({ return new HttpsAgent(
ca: await getAllTrustedCertificatesAsync(), override ?? {
checkServerIdentity: createCustomCheckServerIdentity(await getTrustedCertificateHostnamesAsync()), ca: await getAllTrustedCertificatesAsync(),
// Override the ca and checkServerIdentity if provided checkServerIdentity: createCustomCheckServerIdentity(await getTrustedCertificateHostnamesAsync()),
...override, },
proxyEnv: process.env, );
});
}; };
export const createAxiosCertificateInstanceAsync = async ( export const createAxiosCertificateInstanceAsync = async (
@@ -7,7 +7,7 @@ import { TestLogger } from "../logs";
vi.mock("undici", () => { vi.mock("undici", () => {
return { return {
EnvHttpProxyAgent: class EnvHttpProxyAgent { Agent: class Agent {
dispatch(_options: Dispatcher.DispatchOptions, _handler: Dispatcher.DispatchHandler): boolean { dispatch(_options: Dispatcher.DispatchOptions, _handler: Dispatcher.DispatchHandler): boolean {
return true; return true;
} }
+5 -5
View File
@@ -28,13 +28,13 @@
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0",
"@homarr/cron-jobs": "workspace:^0.1.0", "@homarr/cron-jobs": "workspace:^0.1.0",
"@tanstack/react-query": "^5.90.14", "@tanstack/react-query": "^5.90.12",
"@trpc/client": "^11.8.1", "@trpc/client": "^11.8.0",
"@trpc/server": "^11.8.1", "@trpc/server": "^11.8.0",
"@trpc/tanstack-react-query": "^11.8.1", "@trpc/tanstack-react-query": "^11.8.0",
"node-cron": "^4.2.1", "node-cron": "^4.2.1",
"react": "19.2.3", "react": "19.2.3",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
@@ -121,46 +121,6 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
); );
} }
countDeleted += deadIcons.length;
});
},
handleSync() {
db.transaction((transaction) => {
if (newIconRepositories.length >= 1) {
transaction.insert(iconRepositories).values(newIconRepositories).run();
}
if (newIcons.length >= 1) {
// We only insert 5000 icons at a time to avoid SQLite limitations
for (const chunck of splitToNChunks(newIcons, Math.ceil(newIcons.length / 5000))) {
transaction.insert(icons).values(chunck).run();
}
}
if (deadIcons.length >= 1) {
transaction
.delete(icons)
.where(
inArray(
// Combine iconRepositoryId and checksum to allow same icons on different repositories
sql`concat(${icons.iconRepositoryId}, '.', ${icons.checksum})`,
deadIcons.map((icon) => `${icon.iconRepositoryId}.${icon.checksum}`),
),
)
.run();
}
if (deadIconRepositories.length >= 1) {
transaction
.delete(iconRepositories)
.where(
inArray(
iconRepositories.id,
deadIconRepositories.map((iconRepository) => iconRepository.id),
),
)
.run();
}
countDeleted += deadIcons.length; countDeleted += deadIcons.length;
}); });
}, },
+3 -15
View File
@@ -31,20 +31,8 @@ export const createDbInsertCollectionForTransaction = <TTableKey extends TableKe
return { return {
...context, ...context,
insertAll: (db: HomarrDatabase) => {
db.transaction((transaction) => {
for (const [key, values] of objectEntries(context)) {
if (values.length >= 1) {
transaction
.insert(schema[key])
.values(values as never)
.run();
}
}
});
},
// We allow any database that supports async passed here but then fallback to mysql to prevent typescript errors // We allow any database that supports async passed here but then fallback to mysql to prevent typescript errors
insertAllAsync: async (db: HomarrDatabaseMysql | HomarrDatabasePostgresql) => { insertAllAsync: async (db: HomarrDatabaseMysql | HomarrDatabasePostgresql | HomarrDatabase) => {
const innerDb = db as HomarrDatabaseMysql; const innerDb = db as HomarrDatabaseMysql;
await innerDb.transaction(async (transaction) => { await innerDb.transaction(async (transaction) => {
for (const [key, values] of objectEntries(context)) { for (const [key, values] of objectEntries(context)) {
@@ -61,7 +49,7 @@ export const createDbInsertCollectionForTransaction = <TTableKey extends TableKe
export const createDbInsertCollectionWithoutTransaction = <TTableKey extends TableKey>( export const createDbInsertCollectionWithoutTransaction = <TTableKey extends TableKey>(
tablesInInsertOrder: TTableKey[], tablesInInsertOrder: TTableKey[],
) => { ) => {
const { insertAll, insertAllAsync, ...collection } = createDbInsertCollectionForTransaction(tablesInInsertOrder); const { insertAllAsync, ...collection } = createDbInsertCollectionForTransaction(tablesInInsertOrder);
return { return {
...collection, ...collection,
@@ -75,7 +63,7 @@ export const createDbInsertCollectionWithoutTransaction = <TTableKey extends Tab
default: default:
// For better-sqlite3, we need to use the synchronous insertAll method // For better-sqlite3, we need to use the synchronous insertAll method
// default assumes better-sqlite3. It's original implementation. // default assumes better-sqlite3. It's original implementation.
insertAll(db); await insertAllAsync(db);
break; break;
} }
}, },
+2 -2
View File
@@ -1,4 +1,4 @@
import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3"; import type { LibSQLDatabase } from "drizzle-orm/libsql";
import type { MySql2Database } from "drizzle-orm/mysql2"; import type { MySql2Database } from "drizzle-orm/mysql2";
import type { NodePgDatabase } from "drizzle-orm/node-postgres"; import type { NodePgDatabase } from "drizzle-orm/node-postgres";
@@ -6,6 +6,6 @@ import type * as mysqlSchema from "./schema/mysql";
import type * as pgSchema from "./schema/postgresql"; import type * as pgSchema from "./schema/postgresql";
import type * as sqliteSchema from "./schema/sqlite"; import type * as sqliteSchema from "./schema/sqlite";
export type HomarrDatabase = BetterSQLite3Database<typeof sqliteSchema>; export type HomarrDatabase = LibSQLDatabase<typeof sqliteSchema>;
export type HomarrDatabaseMysql = MySql2Database<typeof mysqlSchema>; export type HomarrDatabaseMysql = MySql2Database<typeof mysqlSchema>;
export type HomarrDatabasePostgresql = NodePgDatabase<typeof pgSchema>; export type HomarrDatabasePostgresql = NodePgDatabase<typeof pgSchema>;
+4 -6
View File
@@ -49,14 +49,13 @@
"@homarr/server-settings": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0",
"@mantine/core": "^8.3.10", "@mantine/core": "^8.3.10",
"@paralleldrive/cuid2": "^3.1.0", "@paralleldrive/cuid2": "^3.1.0",
"@testcontainers/mysql": "^11.11.0", "@testcontainers/mysql": "^11.10.0",
"@testcontainers/postgresql": "^11.11.0", "@testcontainers/postgresql": "^11.10.0",
"better-sqlite3": "^12.5.0",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"drizzle-kit": "^0.31.8", "drizzle-kit": "^0.31.8",
"drizzle-orm": "^0.45.1", "drizzle-orm": "^0.45.1",
"drizzle-zod": "^0.8.3", "drizzle-zod": "^0.8.3",
"mysql2": "3.16.0", "mysql2": "3.15.3",
"pg": "^8.16.3", "pg": "^8.16.3",
"superjson": "2.2.6" "superjson": "2.2.6"
}, },
@@ -64,10 +63,9 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/better-sqlite3": "7.6.13",
"@types/pg": "^8.16.0", "@types/pg": "^8.16.0",
"dotenv-cli": "^11.0.0", "dotenv-cli": "^11.0.0",
"esbuild": "^0.27.2", "esbuild": "^0.27.1",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"prettier": "^3.7.4", "prettier": "^3.7.4",
"tsx": "4.20.4", "tsx": "4.20.4",
+6 -5
View File
@@ -1,15 +1,16 @@
import Database from "better-sqlite3"; import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/better-sqlite3"; import { drizzle } from "drizzle-orm/libsql";
import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import { migrate } from "drizzle-orm/libsql/migrator";
import { DB_CASING } from "@homarr/core/infrastructure/db/constants"; import { DB_CASING } from "@homarr/core/infrastructure/db/constants";
import * as sqliteSchema from "../schema/sqlite"; import * as sqliteSchema from "../schema/sqlite";
export const createDb = (debug?: boolean) => { export const createDb = (debug?: boolean) => {
const sqlite = new Database(":memory:"); const sqlite = createClient({ url: ":memory:" });
const db = drizzle(sqlite, { schema: sqliteSchema, logger: debug, casing: DB_CASING }); const db = drizzle(sqlite, { schema: sqliteSchema, logger: debug, casing: DB_CASING });
migrate(db, { // TODO: Make async and awaitable
void migrate(db, {
migrationsFolder: "./packages/db/migrations/sqlite", migrationsFolder: "./packages/db/migrations/sqlite",
}); });
+1 -8
View File
@@ -1,11 +1,9 @@
import { isMysql, isPostgresql } from "./collection";
import type { HomarrDatabase, HomarrDatabaseMysql } from "./driver"; import type { HomarrDatabase, HomarrDatabaseMysql } from "./driver";
import type { MySqlSchema } from "./schema"; import type { MySqlSchema } from "./schema";
import * as schema from "./schema"; import * as schema from "./schema";
interface HandleTransactionInput { interface HandleTransactionInput {
handleAsync: (db: HomarrDatabaseMysql, schema: MySqlSchema) => Promise<void>; handleAsync: (db: HomarrDatabaseMysql, schema: MySqlSchema) => Promise<void>;
handleSync: (db: HomarrDatabase) => void;
} }
/** /**
@@ -14,10 +12,5 @@ interface HandleTransactionInput {
* But it can also generally be used when dealing with different database drivers. * But it can also generally be used when dealing with different database drivers.
*/ */
export const handleDiffrentDbDriverOperationsAsync = async (db: HomarrDatabase, input: HandleTransactionInput) => { export const handleDiffrentDbDriverOperationsAsync = async (db: HomarrDatabase, input: HandleTransactionInput) => {
if (isMysql() || isPostgresql()) { await input.handleAsync(db as unknown as HomarrDatabaseMysql, schema as unknown as MySqlSchema);
// Schema type is always the correct one based on env variables
await input.handleAsync(db as unknown as HomarrDatabaseMysql, schema as unknown as MySqlSchema);
} else {
input.handleSync(db);
}
}; };
+1 -1
View File
@@ -25,7 +25,7 @@
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"fast-xml-parser": "^5.3.3", "fast-xml-parser": "^5.3.3",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
-7
View File
@@ -298,13 +298,6 @@ export const integrationDefs = {
category: ["healthMonitoring"], category: ["healthMonitoring"],
documentationUrl: createDocumentationLink("/docs/integrations/truenas"), documentationUrl: createDocumentationLink("/docs/integrations/truenas"),
}, },
unraid: {
name: "Unraid",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/unraid.svg",
category: ["healthMonitoring"],
documentationUrl: createDocumentationLink("/docs/integrations/unraid"),
},
// This integration only returns mock data, it is used during development (but can also be used in production by directly going to the create page) // This integration only returns mock data, it is used during development (but can also be used in production by directly going to the create page)
mock: { mock: {
name: "Mock", name: "Mock",
+1 -1
View File
@@ -28,7 +28,7 @@
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/form": "^8.3.10", "@mantine/form": "^8.3.10",
"mantine-form-zod-resolver": "^1.3.0", "mantine-form-zod-resolver": "^1.3.0",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
+1 -1
View File
@@ -32,7 +32,7 @@
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.3.10", "@mantine/core": "^8.3.10",
"react": "19.2.3", "react": "19.2.3",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
+2 -2
View File
@@ -2,8 +2,8 @@ import type { Database } from "@homarr/db";
import { like } from "@homarr/db"; import { like } from "@homarr/db";
import { icons } from "@homarr/db/schema"; import { icons } from "@homarr/db/schema";
export const getIconForName = (db: Database, name: string) => { export const getIconForNameAsync = async (db: Database, name: string) => {
return db.query.icons.findFirst({ return await db.query.icons.findFirst({
where: like(icons.name, `%${name}%`), where: like(icons.name, `%${name}%`),
}); });
}; };
@@ -1,7 +1,6 @@
import { parse } from "path"; import { parse } from "path";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import type { IconRepositoryLicense } from "../types/icon-repository-license"; import type { IconRepositoryLicense } from "../types/icon-repository-license";
import type { RepositoryIconGroup } from "../types/repository-icon-group"; import type { RepositoryIconGroup } from "../types/repository-icon-group";
@@ -20,12 +19,11 @@ export class GitHubIconRepository extends IconRepository {
} }
protected async getAllIconsInternalAsync(): Promise<RepositoryIconGroup> { protected async getAllIconsInternalAsync(): Promise<RepositoryIconGroup> {
const url = this.repositoryIndexingUrl; if (!this.repositoryIndexingUrl || !this.repositoryBlobUrlTemplate) {
if (!url || !this.repositoryBlobUrlTemplate) {
throw new Error("Repository URLs are required for this repository"); throw new Error("Repository URLs are required for this repository");
} }
const response = await withTimeoutAsync(async (signal) => fetchWithTrustedCertificatesAsync(url, { signal })); const response = await fetchWithTimeoutAsync(this.repositoryIndexingUrl);
const listOfFiles = (await response.json()) as GitHubApiResponse; const listOfFiles = (await response.json()) as GitHubApiResponse;
return { return {
@@ -1,7 +1,6 @@
import { parse } from "path"; import { parse } from "path";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import type { IconRepositoryLicense } from "../types/icon-repository-license"; import type { IconRepositoryLicense } from "../types/icon-repository-license";
import type { RepositoryIconGroup } from "../types/repository-icon-group"; import type { RepositoryIconGroup } from "../types/repository-icon-group";
@@ -20,9 +19,7 @@ export class JsdelivrIconRepository extends IconRepository {
} }
protected async getAllIconsInternalAsync(): Promise<RepositoryIconGroup> { protected async getAllIconsInternalAsync(): Promise<RepositoryIconGroup> {
const response = await withTimeoutAsync(async (signal) => const response = await fetchWithTimeoutAsync(this.repositoryIndexingUrl);
fetchWithTrustedCertificatesAsync(this.repositoryIndexingUrl, { signal }),
);
const listOfFiles = (await response.json()) as JsdelivrApiResponse; const listOfFiles = (await response.json()) as JsdelivrApiResponse;
return { return {
+1 -1
View File
@@ -48,7 +48,7 @@
"tsdav": "^2.1.6", "tsdav": "^2.1.6",
"undici": "7.16.0", "undici": "7.16.0",
"xml2js": "^0.6.2", "xml2js": "^0.6.2",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
@@ -38,7 +38,6 @@ import { ProxmoxIntegration } from "../proxmox/proxmox-integration";
import { QuayIntegration } from "../quay/quay-integration"; import { QuayIntegration } from "../quay/quay-integration";
import { TrueNasIntegration } from "../truenas/truenas-integration"; import { TrueNasIntegration } from "../truenas/truenas-integration";
import { UnifiControllerIntegration } from "../unifi-controller/unifi-controller-integration"; import { UnifiControllerIntegration } from "../unifi-controller/unifi-controller-integration";
import { UnraidIntegration } from "../unraid/unraid-integration";
import type { Integration, IntegrationInput } from "./integration"; import type { Integration, IntegrationInput } from "./integration";
export const createIntegrationAsync = async <TKind extends keyof typeof integrationCreators>( export const createIntegrationAsync = async <TKind extends keyof typeof integrationCreators>(
@@ -102,7 +101,6 @@ export const integrationCreators = {
ntfy: NTFYIntegration, ntfy: NTFYIntegration,
mock: MockIntegration, mock: MockIntegration,
truenas: TrueNasIntegration, truenas: TrueNasIntegration,
unraid: UnraidIntegration,
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>; } satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
type IntegrationInstanceOfKind<TKind extends keyof typeof integrationCreators> = { type IntegrationInstanceOfKind<TKind extends keyof typeof integrationCreators> = {
-1
View File
@@ -22,7 +22,6 @@ export { PiHoleIntegrationV6 } from "./pi-hole/v6/pi-hole-integration-v6";
export { PlexIntegration } from "./plex/plex-integration"; export { PlexIntegration } from "./plex/plex-integration";
export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration"; export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration";
export { TrueNasIntegration } from "./truenas/truenas-integration"; export { TrueNasIntegration } from "./truenas/truenas-integration";
export { UnraidIntegration } from "./unraid/unraid-integration";
export { OPNsenseIntegration } from "./opnsense/opnsense-integration"; export { OPNsenseIntegration } from "./opnsense/opnsense-integration";
export { ICalIntegration } from "./ical/ical-integration"; export { ICalIntegration } from "./ical/ical-integration";
@@ -1,189 +0,0 @@
import dayjs from "dayjs";
import type { fetch as undiciFetch } from "undici/types/fetch";
import { humanFileSize } from "@homarr/common";
import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { HandleIntegrationErrors } from "../base/errors/decorator";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { ISystemHealthMonitoringIntegration } from "../interfaces/health-monitoring/health-monitoring-integration";
import type { SystemHealthMonitoring } from "../interfaces/health-monitoring/health-monitoring-types";
import type { UnraidSystemInfo } from "./unraid-types";
import { unraidSystemInfoSchema } from "./unraid-types";
const logger = createLogger({ module: "UnraidIntegration" });
@HandleIntegrationErrors([])
export class UnraidIntegration extends Integration implements ISystemHealthMonitoringIntegration {
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
await this.queryGraphQLAsync<{ info: UnraidSystemInfo }>(
`
query {
info {
os { platform }
}
}
`,
input.fetchAsync,
);
return { success: true };
}
public async getSystemInfoAsync(): Promise<SystemHealthMonitoring> {
const systemInfo = await this.getSystemInformationAsync();
const cpuUtilization = systemInfo.metrics.cpu.cpus.reduce((acc, val) => acc + val.percentTotal, 0);
const cpuCount = systemInfo.info.cpu.cores;
// We use "info" object instead of the stats since this is the exact amount the kernel sees, which is what Unraid displays.
const totalMemory = systemInfo.info.memory.layout.reduce((acc, layout) => layout.size + acc, 0);
const usedMemory = totalMemory * (systemInfo.metrics.memory.percentTotal / 100);
const uptime = dayjs(systemInfo.info.os.uptime);
return {
version: systemInfo.info.os.release,
cpuModelName: systemInfo.info.cpu.brand,
cpuUtilization: cpuUtilization / cpuCount,
memUsedInBytes: usedMemory,
memAvailableInBytes: totalMemory - usedMemory,
uptime: dayjs().diff(uptime, "seconds"),
network: null, // Not implemented, see https://github.com/unraid/api/issues/1602
loadAverage: null,
rebootRequired: false,
availablePkgUpdates: 0,
cpuTemp: undefined, // Not implemented, see https://github.com/unraid/api/issues/1597
fileSystem: systemInfo.array.disks.map((disk) => ({
deviceName: disk.name,
used: humanFileSize(disk.fsUsed * 1024), // API is in KiB (kibibytes), covert to bytes
available: `${disk.size * 1024}`, // API is in KiB (kibibytes), covert to bytes
percentage: (disk.fsUsed / disk.size) * 100, // The units are the same, therefore the actual unit is irrelevant
})),
smart: systemInfo.array.disks.map((disk) => ({
deviceName: disk.name,
temperature: disk.temp,
overallStatus: disk.status,
})),
};
}
private async getSystemInformationAsync(): Promise<UnraidSystemInfo> {
logger.debug("Retrieving system information", {
url: this.url("/graphql"),
});
const query = `
query {
metrics {
cpu {
percentTotal
cpus {
percentTotal
}
},
memory {
available
used
free
total
swapFree
swapTotal
swapUsed
percentTotal
}
}
array {
state
capacity {
disks {
free
total
used
}
}
disks {
name
size
fsFree
fsUsed
status
temp
}
}
info {
devices {
network {
speed
dhcp
model
model
}
}
os {
platform,
distro,
release,
uptime
},
cpu {
manufacturer,
brand,
cores,
threads
},
memory {
layout {
size
}
}
}
}
`;
const response = await this.queryGraphQLAsync<UnraidSystemInfo>(query);
const result = await unraidSystemInfoSchema.parseAsync(response);
logger.debug("Retrieved system information", {
url: this.url("/graphql"),
});
return result;
}
private async queryGraphQLAsync<T>(
query: string,
fetchAsync: typeof undiciFetch = fetchWithTrustedCertificatesAsync,
): Promise<T> {
const url = this.url("/graphql");
const apiKey = this.getSecretValue("apiKey");
logger.debug("Sending GraphQL query", {
url: url.toString(),
});
const response = await fetchAsync(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify({ query }),
});
if (!response.ok) {
throw new ResponseError(response);
}
const json = (await response.json()) as { data: T; errors?: { message: string }[] };
if (json.errors) {
throw new Error(`GraphQL errors: ${json.errors.map((error) => error.message).join(", ")}`);
}
return json.data;
}
}
@@ -1,73 +0,0 @@
import z from "zod";
export const unraidSystemInfoSchema = z.object({
metrics: z.object({
cpu: z.object({
percentTotal: z.number(),
cpus: z.array(
z.object({
percentTotal: z.number(),
}),
),
}),
memory: z.object({
available: z.number(),
used: z.number(),
free: z.number(),
total: z.number().min(0),
percentTotal: z.number().min(0).max(100),
}),
}),
array: z.object({
state: z.string(),
capacity: z.object({
disks: z.object({
free: z.coerce.number(),
total: z.coerce.number(),
used: z.coerce.number(),
}),
}),
disks: z.array(
z.object({
name: z.string(),
size: z.number(),
fsFree: z.number(),
fsUsed: z.number(),
status: z.string(),
temp: z.number(),
}),
),
}),
info: z.object({
devices: z.object({
network: z.array(
z.object({
speed: z.number(),
dhcp: z.boolean(),
model: z.string(),
}),
),
}),
os: z.object({
platform: z.string(),
distro: z.string(),
release: z.string(),
uptime: z.coerce.date(),
}),
cpu: z.object({
manufacturer: z.string(),
brand: z.string(),
cores: z.number(),
threads: z.number(),
}),
memory: z.object({
layout: z.array(
z.object({
size: z.number(),
}),
),
}),
}),
});
export type UnraidSystemInfo = z.infer<typeof unraidSystemInfoSchema>;
+2 -2
View File
@@ -36,10 +36,10 @@
"@mantine/core": "^8.3.10", "@mantine/core": "^8.3.10",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"next": "16.1.1", "next": "16.0.10",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
+2 -2
View File
@@ -40,11 +40,11 @@
"@mantine/core": "^8.3.10", "@mantine/core": "^8.3.10",
"@mantine/hooks": "^8.3.10", "@mantine/hooks": "^8.3.10",
"adm-zip": "0.5.16", "adm-zip": "0.5.16",
"next": "16.1.1", "next": "16.0.10",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"superjson": "2.2.6", "superjson": "2.2.6",
"zod": "^4.2.1", "zod": "^4.1.13",
"zod-form-data": "^3.0.1" "zod-form-data": "^3.0.1"
}, },
"devDependencies": { "devDependencies": {
@@ -45,13 +45,6 @@ export const importInitialOldmarrAsync = async (
await integrationInsertCollection.insertAllAsync(transaction); await integrationInsertCollection.insertAllAsync(transaction);
}); });
}, },
handleSync(db) {
db.transaction((transaction) => {
boardInsertCollection.insertAll(transaction);
userInsertCollection.insertAll(transaction);
integrationInsertCollection.insertAll(transaction);
});
},
}); });
logger.info("Import successful", { duration: stopwatch.getElapsedInHumanWords() }); logger.info("Import successful", { duration: stopwatch.getElapsedInHumanWords() });
@@ -35,8 +35,5 @@ export const importSingleOldmarrConfigAsync = async (
async handleAsync(db) { async handleAsync(db) {
await boardInsertCollection.insertAllAsync(db); await boardInsertCollection.insertAllAsync(db);
}, },
handleSync(db) {
boardInsertCollection.insertAll(db);
},
}); });
}; };
+1 -1
View File
@@ -23,7 +23,7 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
+1 -1
View File
@@ -34,7 +34,7 @@
"octokit": "^5.0.5", "octokit": "^5.0.5",
"superjson": "2.2.6", "superjson": "2.2.6",
"undici": "7.16.0", "undici": "7.16.0",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
+20 -47
View File
@@ -12,7 +12,9 @@ export const dockerContainersRequestHandler = createCachedWidgetRequestHandler({
queryKey: "dockerContainersResult", queryKey: "dockerContainersResult",
widgetKind: "dockerContainers", widgetKind: "dockerContainers",
async requestAsync() { async requestAsync() {
return await getContainersWithStatsAsync(); const containers = await getContainersWithStatsAsync();
return containers;
}, },
cacheDuration: dayjs.duration(20, "seconds"), cacheDuration: dayjs.duration(20, "seconds"),
}); });
@@ -26,7 +28,7 @@ async function getContainersWithStatsAsync() {
dockerInstances.map(async ({ instance, host }) => { dockerInstances.map(async ({ instance, host }) => {
const instanceContainers = await instance.listContainers({ all: true }); const instanceContainers = await instance.listContainers({ all: true });
return instanceContainers return instanceContainers
.filter((container) => !(dockerLabels.hide in container.Labels)) .filter((container) => dockerLabels.hide in container.Labels === false)
.map((container) => ({ ...container, instance: host })); .map((container) => ({ ...container, instance: host }));
}), }),
).then((res) => res.flat()); ).then((res) => res.flat());
@@ -43,21 +45,7 @@ async function getContainersWithStatsAsync() {
const instance = dockerInstances.find(({ host }) => host === container.instance)?.instance; const instance = dockerInstances.find(({ host }) => host === container.instance)?.instance;
if (!instance) return null; if (!instance) return null;
// Get stats, falling back to an empty stats object if fetch fails const stats = await instance.getContainer(container.Id).stats({ stream: false, "one-shot": true });
// calculateCpuUsage and calculateMemoryUsage will return 0 for invalid/missing stats
const stats = await instance
.getContainer(container.Id)
.stats({ stream: false, "one-shot": true })
.catch(
() =>
({
cpu_stats: { online_cpus: 0, cpu_usage: { total_usage: 0 }, system_cpu_usage: 0 },
memory_stats: { usage: 0 },
}) as ContainerStats,
);
const cpuUsage = calculateCpuUsage(stats);
const memoryUsage = calculateMemoryUsage(stats);
return { return {
id: container.Id, id: container.Id,
@@ -69,8 +57,19 @@ async function getContainersWithStatsAsync() {
if (!extractedImage) return false; if (!extractedImage) return false;
return icon.name.toLowerCase().includes(extractedImage.toLowerCase()); return icon.name.toLowerCase().includes(extractedImage.toLowerCase());
})?.url ?? null, })?.url ?? null,
cpuUsage, cpuUsage: calculateCpuUsage(stats),
memoryUsage, // memory usage by default includes cache, which should not be shown as it is also not shown with docker stats command
// The below type is probably wrong, sometimes stats can be undefined
// See https://docs.docker.com/reference/cli/docker/container/stats/ how it is / was calculated
memoryUsage:
stats.memory_stats.usage -
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
(stats.memory_stats.stats?.cache ??
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
stats.memory_stats.stats?.total_inactive_file ??
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
stats.memory_stats.stats?.inactive_file ??
0),
image: container.Image, image: container.Image,
ports: container.Ports, ports: container.Ports,
}; };
@@ -80,36 +79,10 @@ async function getContainersWithStatsAsync() {
} }
function calculateCpuUsage(stats: ContainerStats): number { function calculateCpuUsage(stats: ContainerStats): number {
// Handle containers with missing or invalid stats (e.g., exited, dead containers) const numberOfCpus = stats.cpu_stats.online_cpus || 1;
if (!stats.cpu_stats.online_cpus || stats.cpu_stats.online_cpus === 0 || !stats.cpu_stats.cpu_usage.total_usage) {
return 0;
}
const numberOfCpus = stats.cpu_stats.online_cpus;
const usage = stats.cpu_stats.system_cpu_usage; const usage = stats.cpu_stats.system_cpu_usage;
if (!usage || usage === 0) { if (usage === 0) return 0;
return 0;
}
return (stats.cpu_stats.cpu_usage.total_usage / usage) * numberOfCpus * 100; return (stats.cpu_stats.cpu_usage.total_usage / usage) * numberOfCpus * 100;
} }
function calculateMemoryUsage(stats: ContainerStats): number {
// Handle containers with missing or invalid stats (e.g., exited, dead containers)
if (!stats.memory_stats.usage) {
return 0;
}
// memory usage by default includes cache, which should not be shown as it is also not shown with docker stats command
// See https://docs.docker.com/reference/cli/docker/container/stats/ how it is / was calculated
return (
stats.memory_stats.usage -
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
(stats.memory_stats.stats?.cache ??
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
stats.memory_stats.stats?.total_inactive_file ??
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
stats.memory_stats.stats?.inactive_file ??
0)
);
}
@@ -1,8 +1,7 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
@@ -12,9 +11,7 @@ export const minecraftServerStatusRequestHandler = createCachedWidgetRequestHand
async requestAsync(input: { domain: string; isBedrockServer: boolean }) { async requestAsync(input: { domain: string; isBedrockServer: boolean }) {
const path = `${input.isBedrockServer ? "/bedrock" : ""}/3/${input.domain}`; const path = `${input.isBedrockServer ? "/bedrock" : ""}/3/${input.domain}`;
const response = await withTimeoutAsync(async (signal) => const response = await fetchWithTimeoutAsync(`https://api.mcsrvstat.us${path}`);
fetchWithTrustedCertificatesAsync(`https://api.mcsrvstat.us${path}`, { signal }),
);
return responseSchema.parse(await response.json()); return responseSchema.parse(await response.json());
}, },
cacheDuration: dayjs.duration(5, "minutes"), cacheDuration: dayjs.duration(5, "minutes"),
+4 -8
View File
@@ -1,8 +1,7 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
@@ -10,12 +9,9 @@ export const fetchStockPriceHandler = createCachedWidgetRequestHandler({
queryKey: "fetchStockPriceResult", queryKey: "fetchStockPriceResult",
widgetKind: "stockPrice", widgetKind: "stockPrice",
async requestAsync(input: { stock: string; timeRange: string; timeInterval: string }) { async requestAsync(input: { stock: string; timeRange: string; timeInterval: string }) {
const response = await withTimeoutAsync(async (signal) => { const response = await fetchWithTimeoutAsync(
return await fetchWithTrustedCertificatesAsync( `https://query1.finance.yahoo.com/v8/finance/chart/${input.stock}?range=${input.timeRange}&interval=${input.timeInterval}`,
`https://query1.finance.yahoo.com/v8/finance/chart/${input.stock}?range=${input.timeRange}&interval=${input.timeInterval}`, );
{ signal },
);
});
const data = dataSchema.parse(await response.json()); const data = dataSchema.parse(await response.json());
if ("error" in data) { if ("error" in data) {
@@ -3,7 +3,7 @@ import { Octokit } from "octokit";
import { compareSemVer, isValidSemVer } from "semver-parser"; import { compareSemVer, isValidSemVer } from "semver-parser";
import { env } from "@homarr/common/env"; import { env } from "@homarr/common/env";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { createLogger } from "@homarr/core/infrastructure/logs"; import { createLogger } from "@homarr/core/infrastructure/logs";
import { createChannelWithLatestAndEvents } from "@homarr/redis"; import { createChannelWithLatestAndEvents } from "@homarr/redis";
import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler"; import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler";
@@ -23,7 +23,7 @@ export const updateCheckerRequestHandler = createCachedRequestHandler({
const octokit = new Octokit({ const octokit = new Octokit({
request: { request: {
fetch: fetchWithTrustedCertificatesAsync, fetch: fetchWithTimeoutAsync,
}, },
}); });
const releases = await octokit.rest.repos.listReleases({ const releases = await octokit.rest.repos.listReleases({
+4 -8
View File
@@ -1,8 +1,7 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { z } from "zod"; import { z } from "zod";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
@@ -10,12 +9,9 @@ export const weatherRequestHandler = createCachedWidgetRequestHandler({
queryKey: "weatherAtLocation", queryKey: "weatherAtLocation",
widgetKind: "weather", widgetKind: "weather",
async requestAsync(input: { latitude: number; longitude: number }) { async requestAsync(input: { latitude: number; longitude: number }) {
const res = await withTimeoutAsync(async (signal) => { const res = await fetchWithTimeoutAsync(
return await fetchWithTrustedCertificatesAsync( `https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min,sunrise,sunset,wind_speed_10m_max,wind_gusts_10m_max&current_weather=true&timezone=auto`,
`https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min,sunrise,sunset,wind_speed_10m_max,wind_gusts_10m_max&current_weather=true&timezone=auto`, );
{ signal },
);
});
const json: unknown = await res.json(); const json: unknown = await res.json();
const weather = await atLocationOutput.parseAsync(json); const weather = await atLocationOutput.parseAsync(json);
return { return {
+1 -1
View File
@@ -27,7 +27,7 @@
"@homarr/db": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0",
"@mantine/dates": "^8.3.10", "@mantine/dates": "^8.3.10",
"next": "16.1.1", "next": "16.0.10",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3" "react-dom": "19.2.3"
}, },
+2 -2
View File
@@ -37,8 +37,8 @@
"@mantine/hooks": "^8.3.10", "@mantine/hooks": "^8.3.10",
"@mantine/spotlight": "^8.3.10", "@mantine/spotlight": "^8.3.10",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"jotai": "^2.16.1", "jotai": "^2.16.0",
"next": "16.1.1", "next": "16.0.10",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"use-deep-compare-effect": "^1.8.1" "use-deep-compare-effect": "^1.8.1"
+2 -2
View File
@@ -32,8 +32,8 @@
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"deepmerge": "4.3.1", "deepmerge": "4.3.1",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "16.1.1", "next": "16.0.10",
"next-intl": "4.6.1", "next-intl": "4.6.0",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3" "react-dom": "19.2.3"
}, },
+13 -13
View File
@@ -312,7 +312,7 @@
}, },
"mobileBoard": { "mobileBoard": {
"label": "Mobiles Board", "label": "Mobiles Board",
"description": "Nur Boards, welche für die Gruppe zugänglich sind, können ausgewählt werden" "description": "Nur Boards welche für die Gruppe zugänglich sind, können ausgewählt werden"
} }
}, },
"permission": { "permission": {
@@ -355,7 +355,7 @@
}, },
"view-all": { "view-all": {
"label": "Alle Boards anzeigen", "label": "Alle Boards anzeigen",
"description": "Mitgliedern das Ansehen von Boards erlauben" "description": "Mitgliedern das Ansehen von allen Boards erlauben"
}, },
"modify-all": { "modify-all": {
"label": "Alle Boards ändern", "label": "Alle Boards ändern",
@@ -743,7 +743,7 @@
}, },
"authorization": { "authorization": {
"title": "Autorisierungsfehler", "title": "Autorisierungsfehler",
"description": "Die Anfrage wurde nicht autorisiert. Bitte überprüfen Sie, ob die Anmeldedaten korrekt sind und Sie diese mit den entsprechenden Berechtigungen konfiguriert haben." "description": "Die Anfrage wurde nicht autorisiert. Bitte überprüfen Sie, ob die Anmeldedaten korrekt sind und Sie diese mit mit den entsprechenden Berechtigungen konfiguriert haben."
}, },
"statusCode": { "statusCode": {
"title": "Antwort-Fehler", "title": "Antwort-Fehler",
@@ -964,7 +964,7 @@
}, },
"personalAccessToken": { "personalAccessToken": {
"label": "Persönlicher Zugangs-Token", "label": "Persönlicher Zugangs-Token",
"newLabel": "Neuer persönlicher Zugangs-Token" "newLabel": "Neuer Persönliches Zugangs-Token"
}, },
"topic": { "topic": {
"label": "Thema", "label": "Thema",
@@ -1237,7 +1237,7 @@
}, },
"openAllInNewTabs": { "openAllInNewTabs": {
"title": "Alle in tabs öffnen", "title": "Alle in tabs öffnen",
"text": "Einige Browser können aus Sicherheitsgründen das gleichzeitige Öffnen von mehreren Tabs blockieren. Homarr konnte nicht alle Fenster öffnen, weil Ihr Browser diese Aktion blockiert hat. Bitte erlauben Sie \"Pop-up Fenster öffnen\" und versuchen Sie es erneut." "text": "Einige Browser können aus Sicherheitsgründen das Öffnen von mehreren Tabs aufeinmal blockieren. Homarr konnte nicht alle Fenster öffnen, weil Ihr Browser diese Aktion blockiert hat. Bitte erlauben Sie \"Pop-up Fenster öffnen\" und versuchen Sie es erneut."
} }
} }
}, },
@@ -1949,7 +1949,7 @@
"name": "Name", "name": "Name",
"state": "Status", "state": "Status",
"cpuUsage": "CPU-Auslastung", "cpuUsage": "CPU-Auslastung",
"memoryUsage": "Speichernutzung" "memoryUsage": "Speicherverbrauch"
} }
}, },
"descendingDefaultSort": { "descendingDefaultSort": {
@@ -2639,7 +2639,7 @@
"notification": { "notification": {
"success": { "success": {
"title": "Änderungen erfolgreich angewendet", "title": "Änderungen erfolgreich angewendet",
"message": "Das Board wurde erfolgreich gespeichert" "message": "Das Board wurde erfolgreich dupliziert"
}, },
"error": { "error": {
"title": "Änderungen konnten nicht angewendet werden", "title": "Änderungen konnten nicht angewendet werden",
@@ -2676,7 +2676,7 @@
}, },
"onlyImportApps": { "onlyImportApps": {
"label": "Nur Apps importieren", "label": "Nur Apps importieren",
"description": "Fügt nur die Apps hinzu. Das Board selbst muss manuell neu erstellt werden" "description": "Fügt nur die Apps hinzu, das Board muss manuell neu erstellt werden"
} }
}, },
"name": { "name": {
@@ -2926,7 +2926,7 @@
"error": { "error": {
"noBoard": { "noBoard": {
"title": "Willkommen bei Homarr", "title": "Willkommen bei Homarr",
"description": "Ein schlankes, modernes Dashboard, das alle Ihre Apps und Dienste bereitstellt.", "description": "Ein schlankens, modernes Dashboard, das alle Ihre Apps und Dienste bereit stellt.",
"link": "Erstellen Sie Ihr erstes Board", "link": "Erstellen Sie Ihr erstes Board",
"notice": "Um diese Seite verschwinden zu lassen, erstellen Sie ein Board und setzen es als Home Board" "notice": "Um diese Seite verschwinden zu lassen, erstellen Sie ein Board und setzen es als Home Board"
}, },
@@ -3366,7 +3366,7 @@
"label": "Index Manager" "label": "Index Manager"
}, },
"healthMonitoring": { "healthMonitoring": {
"label": "Überwachung des Zustandes" "label": "Überwachung des Gesundheit"
}, },
"dnsHole": { "dnsHole": {
"label": "DNS Hole Daten" "label": "DNS Hole Daten"
@@ -3512,7 +3512,7 @@
}, },
"about": { "about": {
"version": "Version {version}", "version": "Version {version}",
"text": "Homarr ist ein gemeinschaftsorientiertes Open-Source-Projekt, das von Freiwilligen betreut wird. Dank dieser Menschen ist Homarr seit 2021 ein wachsendes Projekt. Unser Team, dessen Mitglieder in vielen verschiedenen Ländern ansässig sind, arbeitet in seiner Freizeit und ohne Vergütung.", "text": "Homarr ist ein gemeinschaftsorientiertes Open-Source Projekt, das von Freiwilligen betreut wird. Dank dieser Menschen ist Homarr seit 2021 ein wachsendes Projekt. Unser Team, sitzhaft mit ihren Migliedern in vielen verschiedenen Ländern, arbeitet in ihrer Freizeit ohne Vergütung.",
"accordion": { "accordion": {
"contributors": { "contributors": {
"title": "Mitwirkende", "title": "Mitwirkende",
@@ -4395,7 +4395,7 @@
"configControl": "Konfiguration", "configControl": "Konfiguration",
"searchEngineType": { "searchEngineType": {
"generic": "Allgemein", "generic": "Allgemein",
"fromIntegration": "Aus der Integration" "fromIntegration": "Aus den Integration"
} }
}, },
"delete": { "delete": {
@@ -4498,7 +4498,7 @@
"notification": { "notification": {
"success": { "success": {
"title": "Zertifikat entfernt", "title": "Zertifikat entfernt",
"message": "Das Zertifikat wurde erfolgreich entfernt" "message": "Das Zertifikat wurde erfolgreich entfern"
}, },
"error": { "error": {
"title": "Zertifikat nicht entfernt", "title": "Zertifikat nicht entfernt",
+7 -7
View File
@@ -1941,19 +1941,19 @@
"description": "Estadísticas de tus contenedores (Este widget sólo puede ser añadido con privilegios de administrador)", "description": "Estadísticas de tus contenedores (Este widget sólo puede ser añadido con privilegios de administrador)",
"option": { "option": {
"enableRowSorting": { "enableRowSorting": {
"label": "Habilitar ordenar elementos" "label": ""
}, },
"defaultSort": { "defaultSort": {
"label": "Columna usada para ordenar por defecto", "label": "",
"option": { "option": {
"name": "Nombre", "name": "",
"state": "Estado", "state": "",
"cpuUsage": "Uso de CPU", "cpuUsage": "",
"memoryUsage": "Uso de memoria" "memoryUsage": ""
} }
}, },
"descendingDefaultSort": { "descendingDefaultSort": {
"label": "Invertir orden" "label": ""
} }
}, },
"error": { "error": {
+114 -114
View File
@@ -7,7 +7,7 @@
"description": "Pour commencer, veuillez selectionner comment vous souhaitez configurer votre instance Homarr.", "description": "Pour commencer, veuillez selectionner comment vous souhaitez configurer votre instance Homarr.",
"action": { "action": {
"scratch": "Partir de zéro", "scratch": "Partir de zéro",
"importOldmarr": "Importer depuis Homarr avant la 1.0" "importOldmarr": "Importer à partir d'Homarr avant la 1.0"
} }
}, },
"import": { "import": {
@@ -27,7 +27,7 @@
"description": "Configurer le comportement d'importation" "description": "Configurer le comportement d'importation"
}, },
"boardSelection": { "boardSelection": {
"title": "Tableaux {count} trouvés", "title": "{count} tableaux trouvés",
"description": "Choisissez tous les tableaux avec la taille que vous souhaitez importer", "description": "Choisissez tous les tableaux avec la taille que vous souhaitez importer",
"action": { "action": {
"selectAll": "Tout sélectionner", "selectAll": "Tout sélectionner",
@@ -113,7 +113,7 @@
"subtitle": "Bon retour parmi nous ! Veuillez entrer vos identifiants" "subtitle": "Bon retour parmi nous ! Veuillez entrer vos identifiants"
}, },
"invite": { "invite": {
"title": "Rejoignez Homarr", "title": "Rejoindre Homarr",
"subtitle": "Bienvenue sur Homarr! Veuillez créer votre compte", "subtitle": "Bienvenue sur Homarr! Veuillez créer votre compte",
"description": "Vous avez été invité par {username}" "description": "Vous avez été invité par {username}"
}, },
@@ -649,14 +649,14 @@
"app": { "app": {
"option": { "option": {
"existing": { "existing": {
"title": "Existant", "title": "",
"label": "Sélectionner une application existante" "label": ""
}, },
"new": { "new": {
"title": "Nouveau", "title": "",
"url": { "url": {
"label": "Lien de l'application", "label": "",
"description": "Lien d'ouverture de l'application depuis le tableau de bord" "description": ""
} }
} }
} }
@@ -676,9 +676,9 @@
}, },
"app": { "app": {
"action": { "action": {
"add": "Lier une application", "add": "",
"remove": "Délier", "remove": "",
"select": "Sélectionnez une application à lier" "select": ""
} }
} }
}, },
@@ -709,7 +709,7 @@
"description": "L'intégration \"{kind}\" peut être utilisée avec les moteurs de recherche. Cochez ceci pour configurer automatiquement le moteur de recherche." "description": "L'intégration \"{kind}\" peut être utilisée avec les moteurs de recherche. Cochez ceci pour configurer automatiquement le moteur de recherche."
}, },
"app": { "app": {
"sectionTitle": "Application liée" "sectionTitle": ""
}, },
"createApp": { "createApp": {
"label": "Créer une application", "label": "Créer une application",
@@ -991,8 +991,8 @@
"newLabel": "Nouvel ID d'installation" "newLabel": "Nouvel ID d'installation"
}, },
"privateKey": { "privateKey": {
"label": "Clé privée", "label": "",
"newLabel": "Nouvelle clé privée" "newLabel": ""
} }
} }
}, },
@@ -1044,14 +1044,14 @@
} }
}, },
"common": { "common": {
"success": "Succès", "success": "",
"beta": "Bêta", "beta": "Bêta",
"error": "Erreur", "error": "Erreur",
"action": { "action": {
"add": "Ajouter", "add": "Ajouter",
"apply": "Appliquer", "apply": "Appliquer",
"backToOverview": "Retourner à l'aperçu", "backToOverview": "Retourner à l'aperçu",
"change": "Modifier", "change": "",
"create": "Créer", "create": "Créer",
"createAnother": "Créer et recommencer", "createAnother": "Créer et recommencer",
"edit": "Modifier", "edit": "Modifier",
@@ -1174,8 +1174,8 @@
}, },
"unit": { "unit": {
"speed": { "speed": {
"kilometersPerHour": "km/h", "kilometersPerHour": "",
"milesPerHour": "mph" "milesPerHour": ""
} }
} }
}, },
@@ -1190,7 +1190,7 @@
"label": "Titre" "label": "Titre"
}, },
"customCssClasses": { "customCssClasses": {
"label": "Classes Css personnalisées" "label": ""
}, },
"borderColor": { "borderColor": {
"label": "Couleur de la bordure" "label": "Couleur de la bordure"
@@ -1321,21 +1321,21 @@
"label": "Activer la vérification du statut" "label": "Activer la vérification du statut"
}, },
"layout": { "layout": {
"label": "Mise en page", "label": "",
"option": { "option": {
"row": "Horizontale", "row": "",
"row-reverse": "Horizontal (inversé)", "row-reverse": "",
"column": "Vertical", "column": "",
"column-reverse": "Vertical (inversé)" "column-reverse": ""
} }
}, },
"descriptionDisplayMode": { "descriptionDisplayMode": {
"label": "Mode d'affichage de la description", "label": "",
"description": "Choisissez comment afficher la description de l'application", "description": "",
"option": { "option": {
"normal": "Au sein du widget", "normal": "",
"tooltip": "Comme infobulle", "tooltip": "",
"hidden": "Masquer" "hidden": ""
} }
} }
}, },
@@ -1584,11 +1584,11 @@
}, },
"placeholder": "Commencer à écrire vos notes", "placeholder": "Commencer à écrire vos notes",
"dismiss": { "dismiss": {
"title": "Abandonner les modifications ?", "title": "",
"message": "Vous avez des modifications non enregistrées dans votre bloc-notes. Êtes-vous sûr de vouloir les supprimer ?", "message": "",
"action": { "action": {
"discard": "Ignorer les modifications", "discard": "",
"keepEditing": "Continuer les modifications" "keepEditing": ""
} }
} }
}, },
@@ -1743,7 +1743,7 @@
"name": "Calendrier", "name": "Calendrier",
"description": "Afficher les événements de vos intégrations dans une vue calendrier pendant une certaine période de temps relative", "description": "Afficher les événements de vos intégrations dans une vue calendrier pendant une certaine période de temps relative",
"duration": { "duration": {
"allDay": "Tous les jours" "allDay": ""
}, },
"option": { "option": {
"releaseType": { "releaseType": {
@@ -1780,7 +1780,7 @@
"description": "Uniquement sur la météo actuelle" "description": "Uniquement sur la météo actuelle"
}, },
"useImperialSpeed": { "useImperialSpeed": {
"label": "Utiliser mph pour la vitesse du vent" "label": ""
}, },
"location": { "location": {
"label": "Lieu de la météo" "label": "Lieu de la météo"
@@ -1941,19 +1941,19 @@
"description": "Statistiques de vos conteneurs (Ce widget ne peut être ajouté qu'avec les privilèges d'administrateur)", "description": "Statistiques de vos conteneurs (Ce widget ne peut être ajouté qu'avec les privilèges d'administrateur)",
"option": { "option": {
"enableRowSorting": { "enableRowSorting": {
"label": "Activer le tri des éléments" "label": ""
}, },
"defaultSort": { "defaultSort": {
"label": "Colonne de tri par défaut", "label": "",
"option": { "option": {
"name": "Nom", "name": "",
"state": "État", "state": "",
"cpuUsage": "Utilisation du CPU", "cpuUsage": "",
"memoryUsage": "Utilisation mémoire" "memoryUsage": ""
} }
}, },
"descendingDefaultSort": { "descendingDefaultSort": {
"label": "Inverser le tri" "label": ""
} }
}, },
"error": { "error": {
@@ -2036,21 +2036,21 @@
"name": "Nom", "name": "Nom",
"id": "Id", "id": "Id",
"metadata": { "metadata": {
"title": "Statistiques détaillées", "title": "",
"video": { "video": {
"title": "Vidéo", "title": "",
"resolution": "Résolution" "resolution": ""
}, },
"audio": { "audio": {
"title": "Audio", "title": "",
"channelCount": "Canaux audio", "channelCount": "",
"codec": "Codec audio" "codec": ""
}, },
"transcoding": { "transcoding": {
"title": "Transcodage", "title": "",
"container": "Conteneur", "container": "",
"resolution": "Résolution", "resolution": "Résolution",
"target": "Codec cible" "target": ""
} }
} }
} }
@@ -2248,7 +2248,7 @@
"unknown": "Inconnu", "unknown": "Inconnu",
"pending": "En attente", "pending": "En attente",
"processing": "Traitement en cours", "processing": "Traitement en cours",
"requested": "En attente", "requested": "",
"partiallyAvailable": "Partiel", "partiallyAvailable": "Partiel",
"available": "Disponible", "available": "Disponible",
"blacklisted": "Sur la liste noire", "blacklisted": "Sur la liste noire",
@@ -2361,7 +2361,7 @@
"label": "Nombre maximum de publications" "label": "Nombre maximum de publications"
}, },
"hideDescription": { "hideDescription": {
"label": "Masquer la description" "label": ""
} }
} }
}, },
@@ -2542,7 +2542,7 @@
}, },
"firewall": { "firewall": {
"name": "Surveillance du pare-feu", "name": "Surveillance du pare-feu",
"description": "Affiche un résumé des pare-feu", "description": "",
"tab": { "tab": {
"system": "Système", "system": "Système",
"interfaces": "Interfaces" "interfaces": "Interfaces"
@@ -2556,16 +2556,16 @@
"widget": { "widget": {
"fwname": "Nom", "fwname": "Nom",
"version": "Version", "version": "Version",
"versiontitle": "Versions", "versiontitle": "",
"cputitle": "Utilisation du CPU", "cputitle": "",
"memorytitle": "Utilisation de la mémoire", "memorytitle": "",
"cpu": "CPU", "cpu": "",
"memory": "Mémoire", "memory": "",
"interfaces": { "interfaces": {
"name": "nom", "name": "",
"trans": "Transmis", "trans": "",
"recv": "Reçus", "recv": "",
"title": "Interfaces réseau" "title": ""
} }
} }
}, },
@@ -2576,37 +2576,37 @@
"option": {} "option": {}
}, },
"systemResources": { "systemResources": {
"name": "Ressources du système", "name": "",
"description": "CPU, Mémoire, Disque et autre utilisation matérielle de votre système", "description": "",
"option": { "option": {
"hasShadow": { "hasShadow": {
"label": "Activer l'ombrage des cartes" "label": ""
}, },
"visibleCharts": { "visibleCharts": {
"label": "Graphiques visibles", "label": "",
"description": "Sélectionnez les graphiques à afficher.", "description": "",
"option": { "option": {
"cpu": "CPU", "cpu": "",
"memory": "Mémoire", "memory": "",
"network": "Réseau" "network": ""
} }
}, },
"labelDisplayMode": { "labelDisplayMode": {
"label": "Mode d'affichage de l'étiquette", "label": "",
"option": { "option": {
"textWithIcon": "Afficher le texte avec l'icône", "textWithIcon": "",
"text": "Afficher uniquement le texte", "text": "",
"icon": "Afficher uniquement l'icône", "icon": "",
"hidden": "Masquer l'étiquette" "hidden": ""
} }
} }
}, },
"card": { "card": {
"cpu": "CPU", "cpu": "",
"memory": "RAM", "memory": "",
"network": "NET", "network": "",
"up": "UP", "up": "",
"down": "HORS SERVICE" "down": ""
} }
} }
}, },
@@ -3030,8 +3030,8 @@
"integration": "Intégrations", "integration": "Intégrations",
"app": "Applications", "app": "Applications",
"group": "Groupes", "group": "Groupes",
"searchEngine": "Moteurs de recherche", "searchEngine": "",
"media": "Médias" "media": ""
}, },
"statisticLabel": { "statisticLabel": {
"boards": "Tableaux de bord", "boards": "Tableaux de bord",
@@ -3040,8 +3040,8 @@
"authorization": "Autorisation" "authorization": "Autorisation"
}, },
"heroBanner": { "heroBanner": {
"title": "Bienvenue sur votre", "title": "",
"subtitle": "Tableau {app}" "subtitle": ""
} }
}, },
"board": { "board": {
@@ -3387,19 +3387,19 @@
"label": "Conteneurs Docker" "label": "Conteneurs Docker"
}, },
"firewallCpu": { "firewallCpu": {
"label": "Processeur du pare-feu" "label": ""
}, },
"firewallMemory": { "firewallMemory": {
"label": "Mémoire du pare-feu" "label": ""
}, },
"firewallVersion": { "firewallVersion": {
"label": "Version du pare-feu" "label": ""
}, },
"firewallInterfaces": { "firewallInterfaces": {
"label": "Interfaces du pare-feu" "label": ""
}, },
"weather": { "weather": {
"label": "Météo" "label": ""
} }
}, },
"interval": { "interval": {
@@ -3410,10 +3410,10 @@
"weeklyMonday": "Chaque semaine le lundi", "weeklyMonday": "Chaque semaine le lundi",
"update": { "update": {
"success": { "success": {
"message": "Intervalle mis à jour avec succès" "message": ""
}, },
"error": { "error": {
"message": "L'intervalle de mise à jour a échoué" "message": ""
} }
} }
}, },
@@ -3422,55 +3422,55 @@
}, },
"field": { "field": {
"name": { "name": {
"label": "Nom" "label": ""
}, },
"interval": { "interval": {
"label": "Intervalle de planification" "label": "Intervalle de planification"
}, },
"lastExecution": { "lastExecution": {
"label": "Dernière exécution" "label": ""
}, },
"actions": { "actions": {
"label": "Actions" "label": ""
} }
}, },
"table": { "table": {
"search": "Rechercher des tâches {count}..." "search": ""
}, },
"action": { "action": {
"refresh": { "refresh": {
"label": "Rafraîchir" "label": ""
} }
}, },
"refresh": { "refresh": {
"success": { "success": {
"message": "Tâches actualisées avec succès" "message": ""
}, },
"error": { "error": {
"message": "Échec de l'actualisation des tâches" "message": ""
} }
}, },
"trigger": { "trigger": {
"success": { "success": {
"message": "Tâche déclenchée avec succès" "message": ""
}, },
"error": { "error": {
"message": "Échec du déclenchement de la tâche" "message": ""
} }
}, },
"enable": { "enable": {
"success": { "success": {
"message": "Tâche activée avec succès" "message": ""
} }
}, },
"disable": { "disable": {
"success": { "success": {
"message": "Tâche désactivée avec succès" "message": ""
} }
}, },
"toggle": { "toggle": {
"error": { "error": {
"message": "Impossible de basculer le statut de la tâche" "message": ""
} }
} }
}, },
@@ -3527,19 +3527,19 @@
"subtitle": "{count} utilisés dans le Code d'Homarr" "subtitle": "{count} utilisés dans le Code d'Homarr"
}, },
"hotkeys": { "hotkeys": {
"title": "Raccourcis clavier", "title": "",
"subtitle": "Raccourcis clavier pour améliorer votre flux de travail", "subtitle": "",
"field": { "field": {
"shortcut": "Raccourci", "shortcut": "",
"action": "Action" "action": ""
}, },
"action": { "action": {
"toggleBoardEdit": "Activer/désactiver le mode d'édition du tableau", "toggleBoardEdit": "",
"toggleColorScheme": "Basculer en mode clair/sombre", "toggleColorScheme": "",
"saveNotebook": "Enregistrer le bloc-notes (uniquement à l'intérieur du widget notebook)", "saveNotebook": "",
"openSpotlight": "Ouvrir la recherche" "openSpotlight": ""
}, },
"note": "Astuce : Mod correspond à la touche Ctrl ou ⌘ sur macOS" "note": ""
} }
} }
} }
@@ -3995,7 +3995,7 @@
"tools": { "tools": {
"label": "Outils", "label": "Outils",
"tasks": { "tasks": {
"label": "Tâches" "label": ""
}, },
"docker": { "docker": {
"label": "Docker" "label": "Docker"
File diff suppressed because it is too large. Load diff
+34 -34
View File
@@ -177,8 +177,8 @@
} }
}, },
"forgotPassword": { "forgotPassword": {
"label": "Ste pozabili geslo?", "label": "",
"description": "Administrator lahko zažene ukaz:" "description": ""
} }
}, },
"register": { "register": {
@@ -622,7 +622,7 @@
"description": "", "description": "",
"action": "" "action": ""
}, },
"add": "Dodaj aplikacijo" "add": ""
} }
}, },
"integration": { "integration": {
@@ -1059,10 +1059,10 @@
"insert": "Vstavite", "insert": "Vstavite",
"remove": "Odstrani", "remove": "Odstrani",
"save": "Shrani", "save": "Shrani",
"saveChanges": "Shrani spremembe", "saveChanges": "Shranjevanje sprememb",
"cancel": "Prekliči", "cancel": "Prekliči",
"delete": "Izbriši", "delete": "Izbriši",
"discard": "Zapusti", "discard": "",
"close": "", "close": "",
"confirm": "Potrdi", "confirm": "Potrdi",
"continue": "", "continue": "",
@@ -1125,13 +1125,13 @@
"userAvatar": { "userAvatar": {
"menu": { "menu": {
"switchToDarkMode": "", "switchToDarkMode": "",
"switchToLightMode": "Svetli način", "switchToLightMode": "",
"management": "Nastavitve", "management": "",
"preferences": "Prilagoditev", "preferences": "Vaše želje",
"logout": "Odjava", "logout": "",
"login": "Prijava", "login": "Prijava",
"homeBoard": "Vaša nadzorna plošča", "homeBoard": "",
"loggedOut": "Odjava uspešna", "loggedOut": "",
"updateAvailable": "" "updateAvailable": ""
} }
}, },
@@ -1182,7 +1182,7 @@
"section": { "section": {
"dynamic": { "dynamic": {
"action": { "action": {
"create": "Nova dinamična vsebina", "create": "",
"remove": "" "remove": ""
}, },
"option": { "option": {
@@ -1208,7 +1208,7 @@
} }
}, },
"action": { "action": {
"create": "Nova kategorija", "create": "",
"edit": "", "edit": "",
"remove": "", "remove": "",
"moveUp": "Premaknite se navzgor", "moveUp": "Premaknite se navzgor",
@@ -1243,12 +1243,12 @@
}, },
"item": { "item": {
"action": { "action": {
"create": "Nova vsebina", "create": "",
"import": "", "import": "",
"edit": "Uredi gradnik", "edit": "",
"moveResize": "Premakni/spremeni", "moveResize": "",
"duplicate": "Podvoji", "duplicate": "",
"remove": "Izbriši" "remove": ""
}, },
"menu": { "menu": {
"label": { "label": {
@@ -2638,8 +2638,8 @@
"edit": { "edit": {
"notification": { "notification": {
"success": { "success": {
"title": "Spremembe so uveljavljene", "title": "",
"message": "Vsebina je bila uspšeno shranjena" "message": ""
}, },
"error": { "error": {
"title": "", "title": "",
@@ -2647,8 +2647,8 @@
} }
}, },
"confirmLeave": { "confirmLeave": {
"title": "Neshranjene spremembe", "title": "",
"message": "Vsebina ni shranjena, želite nadaljevati?" "message": ""
} }
}, },
"oldImport": { "oldImport": {
@@ -2716,16 +2716,16 @@
}, },
"field": { "field": {
"pageTitle": { "pageTitle": {
"label": "Naslov strani" "label": ""
}, },
"metaTitle": { "metaTitle": {
"label": "Meta naslov" "label": ""
}, },
"logoImageUrl": { "logoImageUrl": {
"label": "URL logotipa" "label": ""
}, },
"faviconImageUrl": { "faviconImageUrl": {
"label": "URL ikone" "label": ""
}, },
"backgroundImageUrl": { "backgroundImageUrl": {
"label": "", "label": "",
@@ -2835,7 +2835,7 @@
"metaTitle": "" "metaTitle": ""
}, },
"setting": { "setting": {
"title": "Nastavitve za {boardName} nadzorno ploščo", "title": "",
"section": { "section": {
"general": { "general": {
"title": "Splošno", "title": "Splošno",
@@ -2844,9 +2844,9 @@
"layout": { "layout": {
"title": "Postavitev", "title": "Postavitev",
"responsive": { "responsive": {
"title": "Predloga", "title": "",
"action": { "action": {
"add": "Dodaj predlogo" "add": ""
} }
} }
}, },
@@ -2959,14 +2959,14 @@
"layout": { "layout": {
"field": { "field": {
"name": { "name": {
"label": "Ime" "label": ""
}, },
"columnCount": { "columnCount": {
"label": "Število stolpcev" "label": ""
}, },
"breakpoint": { "breakpoint": {
"label": "Prekinitvene točke", "label": "",
"description": "Postavitev bo uporabljena na vseh zaslonih, večjih od te prelomne točke, do naslednje večje prelomne točke." "description": ""
} }
} }
}, },
@@ -4046,7 +4046,7 @@
} }
}, },
"search": { "search": {
"placeholder": "Iskanje vsebine", "placeholder": "",
"nothingFound": "", "nothingFound": "",
"error": { "error": {
"fetch": "" "fetch": ""
+1 -1
View File
@@ -34,7 +34,7 @@
"@mantine/hooks": "^8.3.10", "@mantine/hooks": "^8.3.10",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "16.1.1", "next": "16.0.10",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"svgson": "^5.3.1" "svgson": "^5.3.1"
+1 -1
View File
@@ -24,7 +24,7 @@
"dependencies": { "dependencies": {
"@homarr/definitions": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"zod": "^4.2.1", "zod": "^4.1.13",
"zod-form-data": "^3.0.1" "zod-form-data": "^3.0.1"
}, },
"devDependencies": { "devDependencies": {
+18 -18
View File
@@ -52,33 +52,33 @@
"@mantine/core": "^8.3.10", "@mantine/core": "^8.3.10",
"@mantine/hooks": "^8.3.10", "@mantine/hooks": "^8.3.10",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"@tiptap/extension-color": "3.14.0", "@tiptap/extension-color": "3.13.0",
"@tiptap/extension-highlight": "3.14.0", "@tiptap/extension-highlight": "3.13.0",
"@tiptap/extension-image": "3.14.0", "@tiptap/extension-image": "3.13.0",
"@tiptap/extension-link": "^3.14.0", "@tiptap/extension-link": "^3.13.0",
"@tiptap/extension-placeholder": "^3.14.0", "@tiptap/extension-placeholder": "^3.13.0",
"@tiptap/extension-table": "3.14.0", "@tiptap/extension-table": "3.13.0",
"@tiptap/extension-table-cell": "3.14.0", "@tiptap/extension-table-cell": "3.13.0",
"@tiptap/extension-table-header": "3.14.0", "@tiptap/extension-table-header": "3.13.0",
"@tiptap/extension-table-row": "3.14.0", "@tiptap/extension-table-row": "3.13.0",
"@tiptap/extension-task-item": "3.14.0", "@tiptap/extension-task-item": "3.13.0",
"@tiptap/extension-task-list": "3.14.0", "@tiptap/extension-task-list": "3.13.0",
"@tiptap/extension-text-align": "3.14.0", "@tiptap/extension-text-align": "3.13.0",
"@tiptap/extension-text-style": "3.14.0", "@tiptap/extension-text-style": "3.13.0",
"@tiptap/extension-underline": "3.14.0", "@tiptap/extension-underline": "3.13.0",
"@tiptap/react": "^3.14.0", "@tiptap/react": "^3.13.0",
"@tiptap/starter-kit": "^3.14.0", "@tiptap/starter-kit": "^3.13.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"mantine-form-zod-resolver": "^1.3.0", "mantine-form-zod-resolver": "^1.3.0",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "16.1.1", "next": "16.0.10",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"recharts": "^2.15.4", "recharts": "^2.15.4",
"video.js": "^8.23.4", "video.js": "^8.23.4",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
@@ -47,7 +47,7 @@ export const SystemResourceMemoryChart = ({
return ( return (
<Paper px={3} py={2} withBorder shadow="md" radius="md"> <Paper px={3} py={2} withBorder shadow="md" radius="md">
<Text c="dimmed" size="xs"> <Text c="dimmed" size="xs">
{humanFileSize(Math.round(value))} / {humanFileSize(totalCapacityInBytes)} ( {humanFileSize(value)} / {humanFileSize(totalCapacityInBytes)} (
{Math.round((value / totalCapacityInBytes) * 100)}%) {Math.round((value / totalCapacityInBytes) * 100)}%)
</Text> </Text>
</Paper> </Paper>
@@ -22,7 +22,6 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo
}); });
const memoryCapacityInBytes = const memoryCapacityInBytes =
(data[0]?.healthInfo.memAvailableInBytes ?? 0) + (data[0]?.healthInfo.memUsedInBytes ?? 0); (data[0]?.healthInfo.memAvailableInBytes ?? 0) + (data[0]?.healthInfo.memUsedInBytes ?? 0);
const [items, setItems] = useState<{ cpu: number; memory: number; network: { up: number; down: number } | null }[]>( const [items, setItems] = useState<{ cpu: number; memory: number; network: { up: number; down: number } | null }[]>(
data.map((item) => ({ data.map((item) => ({
cpu: item.healthInfo.cpuUtilization, cpu: item.healthInfo.cpuUtilization,
@@ -14,7 +14,7 @@ const labelDisplayModeOptions = {
export const { definition, componentLoader } = createWidgetDefinition("systemResources", { export const { definition, componentLoader } = createWidgetDefinition("systemResources", {
icon: IconGraphFilled, icon: IconGraphFilled,
supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid"], supportedIntegrations: ["dashDot", "openmediavault", "truenas"],
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
hasShadow: factory.switch({ defaultValue: true }), hasShadow: factory.switch({ defaultValue: true }),
+1112 -1192
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1 +1 @@
[{"login":"ajnart","avatar_url":"https://avatars.githubusercontent.com/u/49837342?v=4"},{"login":"manuel-rw","avatar_url":"https://avatars.githubusercontent.com/u/30572287?v=4"},{"login":"Meierschlumpf","avatar_url":"https://avatars.githubusercontent.com/u/63781622?v=4"},{"login":"SeDemal","avatar_url":"https://avatars.githubusercontent.com/u/26098587?v=4"},{"login":"lammersbjorn","avatar_url":"https://avatars.githubusercontent.com/u/71191962?v=4"},{"login":"LarveyOfficial","avatar_url":"https://avatars.githubusercontent.com/u/39219859?v=4"},{"login":"c00ldude1oo","avatar_url":"https://avatars.githubusercontent.com/u/15677803?v=4"},{"login":"InterN0te","avatar_url":"https://avatars.githubusercontent.com/u/10882916?v=4"},{"login":"Aimsucks","avatar_url":"https://avatars.githubusercontent.com/u/1828486?v=4"},{"login":"JannesV","avatar_url":"https://avatars.githubusercontent.com/u/5984244?v=4"},{"login":"MauriceNino","avatar_url":"https://avatars.githubusercontent.com/u/18658092?v=4"},{"login":"hillaliy","avatar_url":"https://avatars.githubusercontent.com/u/950010?v=4"},{"login":"Skeeww","avatar_url":"https://avatars.githubusercontent.com/u/9639992?v=4"},{"login":"RichyHBM","avatar_url":"https://avatars.githubusercontent.com/u/5472275?v=4"},{"login":"Bonfire","avatar_url":"https://avatars.githubusercontent.com/u/5704760?v=4"},{"login":"wieli99","avatar_url":"https://avatars.githubusercontent.com/u/46794825?v=4"},{"login":"ishaanparlikar","avatar_url":"https://avatars.githubusercontent.com/u/43013132?v=4"},{"login":"Zareix","avatar_url":"https://avatars.githubusercontent.com/u/29818713?v=4"},{"login":"tuggan","avatar_url":"https://avatars.githubusercontent.com/u/4325608?v=4"},{"login":"Spillebulle","avatar_url":"https://avatars.githubusercontent.com/u/46653946?v=4"},{"login":"tancak","avatar_url":"https://avatars.githubusercontent.com/u/11760749?v=4"},{"login":"spkesDE","avatar_url":"https://avatars.githubusercontent.com/u/1682314?v=4"},{"login":"j3lte","avatar_url":"https://avatars.githubusercontent.com/u/2557568?v=4"},{"login":"dslatt","avatar_url":"https://avatars.githubusercontent.com/u/5211001?v=4"},{"login":"Malong11-007","avatar_url":"https://avatars.githubusercontent.com/u/40298510?v=4"},{"login":"StefanB7","avatar_url":"https://avatars.githubusercontent.com/u/22056509?v=4"},{"login":"souravghosh01","avatar_url":"https://avatars.githubusercontent.com/u/37548139?v=4"},{"login":"SR3u","avatar_url":"https://avatars.githubusercontent.com/u/2380522?v=4"},{"login":"Rikpat","avatar_url":"https://avatars.githubusercontent.com/u/33869814?v=4"},{"login":"NormanJS","avatar_url":"https://avatars.githubusercontent.com/u/83409549?v=4"},{"login":"No-Maines-Land","avatar_url":"https://avatars.githubusercontent.com/u/20687725?v=4"},{"login":"stark1tty","avatar_url":"https://avatars.githubusercontent.com/u/69326376?v=4"},{"login":"Momcilo42","avatar_url":"https://avatars.githubusercontent.com/u/10118137?v=4"},{"login":"marius-arch","avatar_url":"https://avatars.githubusercontent.com/u/72494359?v=4"},{"login":"Lumilias","avatar_url":"https://avatars.githubusercontent.com/u/10852161?v=4"},{"login":"Havock94","avatar_url":"https://avatars.githubusercontent.com/u/7635248?v=4"},{"login":"Lollyde","avatar_url":"https://avatars.githubusercontent.com/u/11347524?v=4"},{"login":"krishnamuppaneni","avatar_url":"https://avatars.githubusercontent.com/u/4703366?v=4"},{"login":"devtobi","avatar_url":"https://avatars.githubusercontent.com/u/28538704?v=4"},{"login":"TyxTang","avatar_url":"https://avatars.githubusercontent.com/u/90897955?v=4"},{"login":"alotlikebeans","avatar_url":"https://avatars.githubusercontent.com/u/19630511?v=4"},{"login":"WillyJL","avatar_url":"https://avatars.githubusercontent.com/u/49810075?v=4"},{"login":"anonysoul","avatar_url":"https://avatars.githubusercontent.com/u/105715150?v=4"},{"login":"cospeedster","avatar_url":"https://avatars.githubusercontent.com/u/52749645?v=4"},{"login":"diederbert","avatar_url":"https://avatars.githubusercontent.com/u/162878798?v=4"},{"login":"digdilem","avatar_url":"https://avatars.githubusercontent.com/u/4052275?v=4"},{"login":"dinghino","avatar_url":"https://avatars.githubusercontent.com/u/14729459?v=4"},{"login":"gfomichev","avatar_url":"https://avatars.githubusercontent.com/u/1596146?v=4"},{"login":"gnattu","avatar_url":"https://avatars.githubusercontent.com/u/12995396?v=4"},{"login":"tuxpizza","avatar_url":"https://avatars.githubusercontent.com/u/84710786?v=4"},{"login":"xmarkurion","avatar_url":"https://avatars.githubusercontent.com/u/46654687?v=4"},{"login":"huanga","avatar_url":"https://avatars.githubusercontent.com/u/732970?v=4"},{"login":"taos15","avatar_url":"https://avatars.githubusercontent.com/u/122130728?v=4"},{"login":"arghyadipchak","avatar_url":"https://avatars.githubusercontent.com/u/32430374?v=4"},{"login":"AuthorShin","avatar_url":"https://avatars.githubusercontent.com/u/4959043?v=4"},{"login":"benniblot","avatar_url":"https://avatars.githubusercontent.com/uLine truncated [{"login":"ajnart","avatar_url":"https://avatars.githubusercontent.com/u/49837342?v=4"},{"login":"manuel-rw","avatar_url":"https://avatars.githubusercontent.com/u/30572287?v=4"},{"login":"Meierschlumpf","avatar_url":"https://avatars.githubusercontent.com/u/63781622?v=4"},{"login":"SeDemal","avatar_url":"https://avatars.githubusercontent.com/u/26098587?v=4"},{"login":"lammersbjorn","avatar_url":"https://avatars.githubusercontent.com/u/71191962?v=4"},{"login":"LarveyOfficial","avatar_url":"https://avatars.githubusercontent.com/u/39219859?v=4"},{"login":"c00ldude1oo","avatar_url":"https://avatars.githubusercontent.com/u/15677803?v=4"},{"login":"InterN0te","avatar_url":"https://avatars.githubusercontent.com/u/10882916?v=4"},{"login":"Aimsucks","avatar_url":"https://avatars.githubusercontent.com/u/1828486?v=4"},{"login":"JannesV","avatar_url":"https://avatars.githubusercontent.com/u/5984244?v=4"},{"login":"MauriceNino","avatar_url":"https://avatars.githubusercontent.com/u/18658092?v=4"},{"login":"hillaliy","avatar_url":"https://avatars.githubusercontent.com/u/950010?v=4"},{"login":"Skeeww","avatar_url":"https://avatars.githubusercontent.com/u/9639992?v=4"},{"login":"RichyHBM","avatar_url":"https://avatars.githubusercontent.com/u/5472275?v=4"},{"login":"Bonfire","avatar_url":"https://avatars.githubusercontent.com/u/5704760?v=4"},{"login":"wieli99","avatar_url":"https://avatars.githubusercontent.com/u/46794825?v=4"},{"login":"ishaanparlikar","avatar_url":"https://avatars.githubusercontent.com/u/43013132?v=4"},{"login":"Zareix","avatar_url":"https://avatars.githubusercontent.com/u/29818713?v=4"},{"login":"tuggan","avatar_url":"https://avatars.githubusercontent.com/u/4325608?v=4"},{"login":"Spillebulle","avatar_url":"https://avatars.githubusercontent.com/u/46653946?v=4"},{"login":"tancak","avatar_url":"https://avatars.githubusercontent.com/u/11760749?v=4"},{"login":"spkesDE","avatar_url":"https://avatars.githubusercontent.com/u/1682314?v=4"},{"login":"j3lte","avatar_url":"https://avatars.githubusercontent.com/u/2557568?v=4"},{"login":"dslatt","avatar_url":"https://avatars.githubusercontent.com/u/5211001?v=4"},{"login":"Malong11-007","avatar_url":"https://avatars.githubusercontent.com/u/40298510?v=4"},{"login":"StefanB7","avatar_url":"https://avatars.githubusercontent.com/u/22056509?v=4"},{"login":"souravghosh01","avatar_url":"https://avatars.githubusercontent.com/u/37548139?v=4"},{"login":"SR3u","avatar_url":"https://avatars.githubusercontent.com/u/2380522?v=4"},{"login":"Rikpat","avatar_url":"https://avatars.githubusercontent.com/u/33869814?v=4"},{"login":"NormanJS","avatar_url":"https://avatars.githubusercontent.com/u/83409549?v=4"},{"login":"No-Maines-Land","avatar_url":"https://avatars.githubusercontent.com/u/20687725?v=4"},{"login":"stark1tty","avatar_url":"https://avatars.githubusercontent.com/u/69326376?v=4"},{"login":"Momcilo42","avatar_url":"https://avatars.githubusercontent.com/u/10118137?v=4"},{"login":"marius-arch","avatar_url":"https://avatars.githubusercontent.com/u/72494359?v=4"},{"login":"Lumilias","avatar_url":"https://avatars.githubusercontent.com/u/10852161?v=4"},{"login":"Havock94","avatar_url":"https://avatars.githubusercontent.com/u/7635248?v=4"},{"login":"Lollyde","avatar_url":"https://avatars.githubusercontent.com/u/11347524?v=4"},{"login":"krishnamuppaneni","avatar_url":"https://avatars.githubusercontent.com/u/4703366?v=4"},{"login":"devtobi","avatar_url":"https://avatars.githubusercontent.com/u/28538704?v=4"},{"login":"TyxTang","avatar_url":"https://avatars.githubusercontent.com/u/90897955?v=4"},{"login":"alotlikebeans","avatar_url":"https://avatars.githubusercontent.com/u/19630511?v=4"},{"login":"WillyJL","avatar_url":"https://avatars.githubusercontent.com/u/49810075?v=4"},{"login":"anonysoul","avatar_url":"https://avatars.githubusercontent.com/u/105715150?v=4"},{"login":"cospeedster","avatar_url":"https://avatars.githubusercontent.com/u/52749645?v=4"},{"login":"diederbert","avatar_url":"https://avatars.githubusercontent.com/u/162878798?v=4"},{"login":"digdilem","avatar_url":"https://avatars.githubusercontent.com/u/4052275?v=4"},{"login":"dinghino","avatar_url":"https://avatars.githubusercontent.com/u/14729459?v=4"},{"login":"gfomichev","avatar_url":"https://avatars.githubusercontent.com/u/1596146?v=4"},{"login":"gnattu","avatar_url":"https://avatars.githubusercontent.com/u/12995396?v=4"},{"login":"tuxpizza","avatar_url":"https://avatars.githubusercontent.com/u/84710786?v=4"},{"login":"xmarkurion","avatar_url":"https://avatars.githubusercontent.com/u/46654687?v=4"},{"login":"huanga","avatar_url":"https://avatars.githubusercontent.com/u/732970?v=4"},{"login":"taos15","avatar_url":"https://avatars.githubusercontent.com/u/122130728?v=4"},{"login":"arghyadipchak","avatar_url":"https://avatars.githubusercontent.com/u/32430374?v=4"},{"login":"AuthorShin","avatar_url":"https://avatars.githubusercontent.com/u/4959043?v=4"},{"login":"benniblot","avatar_url":"https://avatars.githubusercontent.com/uLine truncated
+1 -1
View File
@@ -1 +1 @@
[{"username":"carlchina","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/6697/medium/d22bbe7797bbeb30dbdc73a5648d329a_default.png"},{"username":"zielmann","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/7795/medium/ad22b8b8d5eb33e4154d53a454c862fd_default.png"},{"username":"Thalyn","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12494028/medium/5faf5acc945a874ed116c4567edaa09f_default.png"},{"username":"magnushj","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12503488/medium/68f32e33ea7b891960055c9f831f7344_default.png"},{"username":"almontegil","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12580457/medium/f4136cacbdfdb4c28ae7f85dc5f840db_default.png"},{"username":"andibing","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12664938/medium/b8be63e4dcb2e791ced1ffc9e3a049a5.jpg"},{"username":"gronare","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12680911/medium/c6eb14d1504156f34a158e507f73aa3d_default.png"},{"username":"SmartPhoneLover","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12701640/medium/ec95486662ec875cda080e778c3ff702.jpg"},{"username":"marcelotk_15","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12773227/medium/ecf6bea16ab0c873c131df2154ee9718_default.png"},{"username":"S3OD177","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13051544/medium/11dd990837f61f7e8f7d36b041a7fddb_default.png"},{"username":"marcg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13153301/medium/ebcf60fbf27deb148ec40a659566c6ff_default.png"},{"username":"BeersTeddy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13185230/medium/2f1f4e1effe74a23422b195cbefb2a95_default.png"},{"username":"jamesmcmahon0","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13245578/medium/586aa873b4abddbd9abc6f3de99ab70e.jpeg"},{"username":"b1nar10","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13286752/medium/249b02e3368752cb175446082015737c_default.png"},{"username":"vannCN","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13330448/medium/9b8a9ee3611e51e951e22d5fd4eb7d8d.jpg"},{"username":"binswm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13343482/medium/45dde0e6097b9b72705d2eba9dbbc276_default.png"},{"username":"SkewRam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13496556/medium/188f5c2deb7938eda51eb786cc4539ca.jpeg"},{"username":"raphcatarino","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13547726/medium/b003511e67df13a4b4b5689488fa8099.jpg"},{"username":"shillos5","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13600557/medium/b17650b2b84f829d54fa548c44c1ab27.jpeg"},{"username":"wolong98","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13641407/medium/f4634edc58c7857a357e5293543c15cf.jpg"},{"username":"spair0039","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14012333/medium/72430e96027c09c19141cac38eae4617.png"},{"username":"MajorMarcin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14072183/medium/0a197e64bb92b6ea94cecbb607512c45.jpg"},{"username":"KosmoMoustache","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14103535/medium/51c438cdd9f86e8811c75f4869de479c.png"},{"username":"Soochaehwa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14118689/medium/496a1ce63111547bf455a1e0a7ac75f1_default.png"},{"username":"garryfield","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14187035/medium/520c964706676045f6eeec1b08edc880.jpg"},{"username":"droidenko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14444264/medium/4aa3a8e824f72bc9e5ec0d8de307095e.jpeg"},{"username":"maathis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14489950/medium/6eeddf13a757ee35f78f1763f94d95e1.jpeg"},{"username":"bukvam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14546502/medium/a819dc40a13c57395a17dc46e847d3ce.jpg"},{"username":"gm.cinalli","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14670666/medium/2d466a0fdbda40764526be86c97c0ab4.jpeg"},{"username":"antoine2tt","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14722148/medium/f88d926900862dd59007ea4b3419cb9d.png"},{"username":"cretzen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14799754/medium/3ba5c1805ec20c000d5810c07985afe9.jpg"},{"username":"ktKongTong","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14817246/medium/97cfc3c028dbdaf85ebd1102da71e58c.jpeg"},{"username":"marinkaberg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14861042/medium/2d5c4e62613f03082f3e645fa92efd59.jpeg"},{"username":"djismgaming","avatarUrl":"httpsLine truncated [{"username":"carlchina","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/6697/medium/d22bbe7797bbeb30dbdc73a5648d329a_default.png"},{"username":"zielmann","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/7795/medium/ad22b8b8d5eb33e4154d53a454c862fd_default.png"},{"username":"Thalyn","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12494028/medium/5faf5acc945a874ed116c4567edaa09f_default.png"},{"username":"magnushj","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12503488/medium/68f32e33ea7b891960055c9f831f7344_default.png"},{"username":"almontegil","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12580457/medium/f4136cacbdfdb4c28ae7f85dc5f840db_default.png"},{"username":"Skick","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12619811/medium/0331f1de413a645b4516bc4e037f31bb.jpg"},{"username":"andibing","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12664938/medium/b8be63e4dcb2e791ced1ffc9e3a049a5.jpg"},{"username":"gronare","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12680911/medium/c6eb14d1504156f34a158e507f73aa3d_default.png"},{"username":"SmartPhoneLover","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12701640/medium/ec95486662ec875cda080e778c3ff702.jpg"},{"username":"marcelotk_15","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12773227/medium/ecf6bea16ab0c873c131df2154ee9718_default.png"},{"username":"S3OD177","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13051544/medium/11dd990837f61f7e8f7d36b041a7fddb_default.png"},{"username":"marcg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13153301/medium/ebcf60fbf27deb148ec40a659566c6ff_default.png"},{"username":"BeersTeddy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13185230/medium/2f1f4e1effe74a23422b195cbefb2a95_default.png"},{"username":"jamesmcmahon0","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13245578/medium/586aa873b4abddbd9abc6f3de99ab70e.jpeg"},{"username":"b1nar10","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13286752/medium/249b02e3368752cb175446082015737c_default.png"},{"username":"vannCN","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13330448/medium/9b8a9ee3611e51e951e22d5fd4eb7d8d.jpg"},{"username":"binswm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13343482/medium/45dde0e6097b9b72705d2eba9dbbc276_default.png"},{"username":"SkewRam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13496556/medium/188f5c2deb7938eda51eb786cc4539ca.jpeg"},{"username":"raphcatarino","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13547726/medium/b003511e67df13a4b4b5689488fa8099.jpg"},{"username":"shillos5","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13600557/medium/b17650b2b84f829d54fa548c44c1ab27.jpeg"},{"username":"wolong98","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13641407/medium/f4634edc58c7857a357e5293543c15cf.jpg"},{"username":"spair0039","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14012333/medium/72430e96027c09c19141cac38eae4617.png"},{"username":"MajorMarcin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14072183/medium/0a197e64bb92b6ea94cecbb607512c45.jpg"},{"username":"KosmoMoustache","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14103535/medium/51c438cdd9f86e8811c75f4869de479c.png"},{"username":"Soochaehwa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14118689/medium/496a1ce63111547bf455a1e0a7ac75f1_default.png"},{"username":"garryfield","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14187035/medium/520c964706676045f6eeec1b08edc880.jpg"},{"username":"droidenko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14444264/medium/4aa3a8e824f72bc9e5ec0d8de307095e.jpeg"},{"username":"maathis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14489950/medium/6eeddf13a757ee35f78f1763f94d95e1.jpeg"},{"username":"bukvam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14546502/medium/a819dc40a13c57395a17dc46e847d3ce.jpg"},{"username":"gm.cinalli","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14670666/medium/2d466a0fdbda40764526be86c97c0ab4.jpeg"},{"username":"antoine2tt","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14722148/medium/f88d926900862dd59007ea4b3419cb9d.png"},{"username":"cretzen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14799754/medium/3ba5c1805ec20c000d5810c07985afe9.jpg"},{"username":"ktKongTong","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14817246/medium/97cfc3c028dbdaf85ebd1102da71e58c.jpeg"},{"username":"marinkaberg","avatarUrl":"https://crowLine truncated
+3 -3
View File
@@ -17,14 +17,14 @@
}, },
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@next/eslint-plugin-next": "16.1.1", "@next/eslint-plugin-next": "16.0.10",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-config-turbo": "^2.7.2", "eslint-config-turbo": "^2.6.3",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^6.1.1", "eslint-plugin-react-hooks": "^6.1.1",
"typescript-eslint": "^8.50.1" "typescript-eslint": "^8.49.0"
}, },
"devDependencies": { "devDependencies": {
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",