Compare commits

...
54 Commits
Author SHA1 Message Date
Releases Homarr cb54de524b chore(release): 1.1.0 [skip ci] 2025-01-22 19:46:18 +00:00
homarr-releases[bot] 7d8c7b9c2d chore(release): automatic release v1.1.0 2025-01-22 19:45:14 +00:00
Meier Lukas 73f7d885cd fix: docker hosts and ports env variable wrongly used (#2050)
* fix: docker hosts and ports env variable wrongly used

* fix: ci issues
2025-01-22 20:43:54 +01:00
Meier Lukas 470d27e091 fix: hide current language switch in other users preferences (#2069) 2025-01-22 20:06:40 +01:00
Meier Lukas 99c8e5b609 fix: add missing request handler for rss feeds (#2006)
* fix: add missing request handler for rss feeds

* fix: ci issues
2025-01-22 18:23:49 +01:00
Meier Lukas b413e2ec7d fix: change error when section for item not found to logged warning and remove item (#2044) 2025-01-22 18:18:14 +01:00
Meier Lukas 29d4f9e003 fix: entity state and execute automation cards aren't movable (#2043)
* fix: entity state and execute automation cards aren't movable

* fix: disable eslint issue
2025-01-22 18:17:15 +01:00
homarr-renovate[bot] 1f54bf5b9e fix(deps): update tiptap monorepo to v2.11.3 (#2067)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-22 16:39:48 +00:00
homarr-renovate[bot] 4d8c22a4e6 fix(deps): update nextjs monorepo to ^15.1.6 (#2063)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-22 11:16:22 +00:00
homarr-renovate[bot] 3e9e1e5e4f fix(deps): update dependency undici to v7.3.0 (#2061)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-22 10:35:59 +00:00
homarr-renovate[bot] 582b981e69 chore(deps): update dependency proxmox-api&gt;undici to v7.3.0 (#2060)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-22 10:15:15 +00:00
homarr-crowdin[bot] f8ec4a43cf chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-22 00:44:52 +00:00
homarr-renovate[bot] 2a8809551c fix(deps): update dependency @tabler/icons-react to ^3.29.0 (#2057)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-21 21:40:00 +00:00
homarr-renovate[bot] 9df59296ff chore(deps): update node.js to v22.13.1 (#2053)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-21 17:16:47 +00:00
homarr-renovate[bot] 701538fcb5 fix(deps): update testcontainers-node monorepo to ^10.17.1 (#2047)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-21 14:47:05 +00:00
homarr-renovate[bot] 0e82f2d199 chore(deps): update dependency testcontainers to ^10.17.0 (#2046)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-21 14:36:28 +00:00
homarr-renovate[bot] a2b5d60aa8 chore(deps): update vitest monorepo to ^3.0.3 (#2045)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-21 14:16:33 +00:00
Yossi Hillali 02dabfcd01 feat: add dropdown list for switching boards (#2042) 2025-01-21 14:34:41 +01:00
Meier Lukas 931f312291 fix: unable to move items with touch devices (#2038) 2025-01-21 13:54:44 +01:00
Meier Lukas 175927970b fix: empty env variables should be ignored (#2040)
* fix: empty env variables should be ignored

* fix: e2e test fails because of empty env variable values
2025-01-21 13:09:49 +01:00
Meier Lukas c922c5b083 fix: certificate card delete button not reachable caused by overflow (#2037) 2025-01-21 11:03:48 +01:00
Meier Lukas 73bbcfeee8 fix: url of batch link can get to long with 414 http code (#2036) 2025-01-21 11:03:16 +01:00
Meier Lukas 7a2d97ba91 fix: copy only working for https and localhost, adding open in new tab (#2035) 2025-01-21 11:03:03 +01:00
Meier Lukas e47d497560 chore: add stack and cause to trpc error logging (#2033) 2025-01-21 11:02:33 +01:00
Meier Lukas b9271ba14a chore: make error messages for auth errors more detailed (#2041) 2025-01-21 11:02:21 +01:00
Meier Lukas 9d85677046 feat: add support for calendar without integrations, add today mark (#2016)
* feat: add support for calendar without integrations, add today mark

* fix: ci issues
2025-01-21 11:01:52 +01:00
Meier Lukas b30c192b8f feat: add initial focus on new integration search (#2015) 2025-01-21 11:00:56 +01:00
Meier Lukas d4f04da709 fix: add item does not respect dynamic sections (#2010)
* bug: add item does not respect dynamic sections

* fix: deepsource issue
2025-01-21 11:00:29 +01:00
Meier Lukas 7960a4cea1 fix: unable to change home board in user preferences (#2011) 2025-01-21 10:59:04 +01:00
homarr-crowdin[bot] 243d7e9dd4 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-21 09:23:53 +00:00
Yossi Hillali bdefff636b fix: health monitoring cpu temp ring when no data, disks health when no data (#2029) 2025-01-20 21:43:18 +01:00
homarr-renovate[bot] 55feb9dff8 fix(deps): update dependency typescript-eslint to ^8.21.0 (#2032)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-20 17:33:27 +00:00
homarr-renovate[bot] 45af6442a1 fix(deps): update dependency jotai to ^2.11.1 (#2026)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-20 14:15:54 +00:00
homarr-crowdin[bot] 72debef4b4 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-20 05:32:12 +00:00
Meier Lukas 25635db17e chore: update homarr-docs-sitemap 2025-01-19 10:56:20 +01:00
homarr-renovate[bot] 1897752e38 fix(deps): update mantine monorepo to ^7.16.1 (#2014)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-19 09:53:43 +00:00
homarr-renovate[bot] 554853ffbd fix(deps): update tanstack-query monorepo to ^5.64.2 (#2013)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-19 08:53:10 +00:00
homarr-crowdin[bot] e40237e4a5 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-19 00:47:05 +00:00
SeDemalandMeier Lukas fa6262cf5e fix: User options switch on load value always false (#1995)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-01-18 08:48:07 +00:00
homarr-renovate[bot] 49bd02e869 fix(deps): update dependency better-sqlite3 to ^11.8.1 (#2005)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-18 06:52:55 +00:00
homarr-crowdin[bot] 3c8fbe371b chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-18 00:42:34 +00:00
Releases Homarr 81a8cf4b36 chore(release): 1.0.1 [skip ci] 2025-01-17 19:14:12 +00:00
homarr-releases[bot] b5c008b4ad chore(release): automatic release v1.0.1 2025-01-17 19:13:04 +00:00
Toine Rademacher (toineenzo)andMeier Lukas 343438391b chore: Fixed broken links in README.md (#1985)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-01-17 17:39:40 +00:00
Toine Rademacher (toineenzo) 8400b9fb11 chore: Change icon and issue link to new repo in layout.tsx and index.tsx (#1986) 2025-01-17 17:35:20 +00:00
Meier Lukas fe65e73fff chore: add version 1.0.0 to bug issue template (#1983) 2025-01-17 18:23:58 +01:00
Meier Lukas e023edf9d6 fix: add missing old-import proxmox integration kind mapping (#1979) 2025-01-17 16:12:02 +01:00
Meier Lukas 927913cf4e fix: add error from ldap bind operation to log statement (#1980) 2025-01-17 16:11:50 +01:00
Meier Lukas 07d8103051 chore: update docs sitemap (#1982) 2025-01-17 16:11:29 +01:00
homarr-renovate[bot] fe08ccf589 chore(deps): update vitest monorepo to v3 (major) (#1964)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-17 16:10:59 +01:00
homarr-renovate[bot] c55207bd59 fix(deps): update dependency flag-icons to ^7.3.2 (#1981)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-17 15:09:46 +00:00
homarr-renovate[bot] e35811c921 fix(deps): update nextjs monorepo to ^15.1.5 (#1978)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-17 15:09:21 +00:00
Meier Lukas e481e116ff Merge branch 'main' into dev 2025-01-17 13:52:59 +01:00
Meier Lukas 32be09a4a7 docs: remove beta banner from readme 2025-01-17 13:51:34 +01:00
113 changed files with 5234 additions and 2559 deletions

No files matched your search

+2 -1
View File
@@ -32,6 +32,7 @@ body:
description: What version of Homarr are you running?
options:
- 1.0.0-beta
- 1.0.0
- Other (describe in "additional information")
default: 0
validations:
@@ -75,4 +76,4 @@ body:
- Other (describe in "additional information")
default: 0
validations:
required: false
required: false
+1 -1
View File
@@ -1 +1 @@
22.13.0
22.13.1
+44
View File
@@ -1,3 +1,47 @@
## [1.1.0](https://github.com/homarr-labs/homarr/compare/v1.0.1...v1.1.0) (2025-01-22)
### Features
* add dropdown list for switching boards ([#2042](https://github.com/homarr-labs/homarr/issues/2042)) ([02dabfc](https://github.com/homarr-labs/homarr/commit/02dabfcd01e372ada7d80a0501b708b171c8fbc6))
* add initial focus on new integration search ([#2015](https://github.com/homarr-labs/homarr/issues/2015)) ([b30c192](https://github.com/homarr-labs/homarr/commit/b30c192b8fbbbfcff8eb09b8f1654eb8dd90b56d))
* add support for calendar without integrations, add today mark ([#2016](https://github.com/homarr-labs/homarr/issues/2016)) ([9d85677](https://github.com/homarr-labs/homarr/commit/9d8567704670a9ab471ac3e246c5ba85f908cfd9))
### Bug Fixes
* add item does not respect dynamic sections ([#2010](https://github.com/homarr-labs/homarr/issues/2010)) ([d4f04da](https://github.com/homarr-labs/homarr/commit/d4f04da70910b6adbf3764de1499dbd46fe9b6d5))
* add missing request handler for rss feeds ([#2006](https://github.com/homarr-labs/homarr/issues/2006)) ([99c8e5b](https://github.com/homarr-labs/homarr/commit/99c8e5b609b17dfe443b35bfff2b883845dc62fe))
* certificate card delete button not reachable caused by overflow ([#2037](https://github.com/homarr-labs/homarr/issues/2037)) ([c922c5b](https://github.com/homarr-labs/homarr/commit/c922c5b08387b01136332876782fed1444203096))
* change error when section for item not found to logged warning and remove item ([#2044](https://github.com/homarr-labs/homarr/issues/2044)) ([b413e2e](https://github.com/homarr-labs/homarr/commit/b413e2ec7dd37c43665373be914ffa414599a206))
* copy only working for https and localhost, adding open in new tab ([#2035](https://github.com/homarr-labs/homarr/issues/2035)) ([7a2d97b](https://github.com/homarr-labs/homarr/commit/7a2d97ba9119e046c06365146fc4efd080f6f6df))
* **deps:** update dependency @tabler/icons-react to ^3.29.0 ([#2057](https://github.com/homarr-labs/homarr/issues/2057)) ([2a88095](https://github.com/homarr-labs/homarr/commit/2a8809551cfc4f4c09aa5c01cac05914fa913769))
* **deps:** update dependency better-sqlite3 to ^11.8.1 ([#2005](https://github.com/homarr-labs/homarr/issues/2005)) ([49bd02e](https://github.com/homarr-labs/homarr/commit/49bd02e869081ec325bd8b4ba2909603a82cd7df))
* **deps:** update dependency jotai to ^2.11.1 ([#2026](https://github.com/homarr-labs/homarr/issues/2026)) ([45af644](https://github.com/homarr-labs/homarr/commit/45af6442a1b84be399ddd281a17227a4184490be))
* **deps:** update dependency typescript-eslint to ^8.21.0 ([#2032](https://github.com/homarr-labs/homarr/issues/2032)) ([55feb9d](https://github.com/homarr-labs/homarr/commit/55feb9dff87088d7f6d006bcecc61b0435edbb6f))
* **deps:** update dependency undici to v7.3.0 ([#2061](https://github.com/homarr-labs/homarr/issues/2061)) ([3e9e1e5](https://github.com/homarr-labs/homarr/commit/3e9e1e5e4f737f8bdb1046585463c987fe43c3d5))
* **deps:** update mantine monorepo to ^7.16.1 ([#2014](https://github.com/homarr-labs/homarr/issues/2014)) ([1897752](https://github.com/homarr-labs/homarr/commit/1897752e3893804a5082f34b20929ef007825506))
* **deps:** update nextjs monorepo to ^15.1.6 ([#2063](https://github.com/homarr-labs/homarr/issues/2063)) ([4d8c22a](https://github.com/homarr-labs/homarr/commit/4d8c22a4e684b4f921967992d04d2461eb2e52db))
* **deps:** update tanstack-query monorepo to ^5.64.2 ([#2013](https://github.com/homarr-labs/homarr/issues/2013)) ([554853f](https://github.com/homarr-labs/homarr/commit/554853ffbda202eff559b5dafa330a15e2649f17))
* **deps:** update testcontainers-node monorepo to ^10.17.1 ([#2047](https://github.com/homarr-labs/homarr/issues/2047)) ([701538f](https://github.com/homarr-labs/homarr/commit/701538fcb5c546478f5d31b2869b933e7544cc3e))
* **deps:** update tiptap monorepo to v2.11.3 ([#2067](https://github.com/homarr-labs/homarr/issues/2067)) ([1f54bf5](https://github.com/homarr-labs/homarr/commit/1f54bf5b9e24b6d063f5ce9165c87d2d036fb8c6))
* docker hosts and ports env variable wrongly used ([#2050](https://github.com/homarr-labs/homarr/issues/2050)) ([73f7d88](https://github.com/homarr-labs/homarr/commit/73f7d885cddbbaa2ac879953fe5ee24a6abe582c))
* empty env variables should be ignored ([#2040](https://github.com/homarr-labs/homarr/issues/2040)) ([1759279](https://github.com/homarr-labs/homarr/commit/175927970b25648104fac5a31649471b3ebeaa9e))
* entity state and execute automation cards aren't movable ([#2043](https://github.com/homarr-labs/homarr/issues/2043)) ([29d4f9e](https://github.com/homarr-labs/homarr/commit/29d4f9e00313fe3e7ae207547cbd7a143b314535))
* health monitoring cpu temp ring when no data, disks health when no data ([#2029](https://github.com/homarr-labs/homarr/issues/2029)) ([bdefff6](https://github.com/homarr-labs/homarr/commit/bdefff636b06cb33020765e606bfe8e41c931722))
* hide current language switch in other users preferences ([#2069](https://github.com/homarr-labs/homarr/issues/2069)) ([470d27e](https://github.com/homarr-labs/homarr/commit/470d27e091a006af8a8c3ceccfc439902ebc5f18))
* unable to change home board in user preferences ([#2011](https://github.com/homarr-labs/homarr/issues/2011)) ([7960a4c](https://github.com/homarr-labs/homarr/commit/7960a4cea11d839f5a0d1889e98bb949c11b68fa))
* unable to move items with touch devices ([#2038](https://github.com/homarr-labs/homarr/issues/2038)) ([931f312](https://github.com/homarr-labs/homarr/commit/931f312291c0a6f1f22a26adf89dfbf912e4fe5e))
* url of batch link can get to long with 414 http code ([#2036](https://github.com/homarr-labs/homarr/issues/2036)) ([73bbcfe](https://github.com/homarr-labs/homarr/commit/73bbcfeee86bf0950145070a6a2ce528b708fe7c))
* User options switch on load value always false ([#1995](https://github.com/homarr-labs/homarr/issues/1995)) ([fa6262c](https://github.com/homarr-labs/homarr/commit/fa6262cf5e780fc3c5b445f988de9d9eb5b86ab5))
## [1.0.1](https://github.com/homarr-labs/homarr/compare/v1.0.0...v1.0.1) (2025-01-17)
### Bug Fixes
* add error from ldap bind operation to log statement ([#1980](https://github.com/homarr-labs/homarr/issues/1980)) ([927913c](https://github.com/homarr-labs/homarr/commit/927913cf4e0a6e4f3a6b7dbf9ae9034d0780b5fb))
* add missing old-import proxmox integration kind mapping ([#1979](https://github.com/homarr-labs/homarr/issues/1979)) ([e023edf](https://github.com/homarr-labs/homarr/commit/e023edf9d62417b054eb565a7f794267539eb992))
* **deps:** update dependency flag-icons to ^7.3.2 ([#1981](https://github.com/homarr-labs/homarr/issues/1981)) ([c55207b](https://github.com/homarr-labs/homarr/commit/c55207bd59f0c51869e8fdb36e083a8327ca46b1))
* **deps:** update nextjs monorepo to ^15.1.5 ([#1978](https://github.com/homarr-labs/homarr/issues/1978)) ([e35811c](https://github.com/homarr-labs/homarr/commit/e35811c921aea8c96dca398841ece396d71d4979))
## 1.0.0 (2025-01-17)
### ⚠ BREAKING CHANGES
+1
View File
@@ -2,6 +2,7 @@
import "@homarr/auth/env";
import "@homarr/db/env";
import "@homarr/common/env";
import "@homarr/docker/env";
import type { NextConfig } from "next";
import MillionLint from "@million/lint";
+16 -14
View File
@@ -23,8 +23,10 @@
"@homarr/cron-job-status": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/docker": "workspace:^0.1.0",
"@homarr/form": "workspace:^0.1.0",
"@homarr/gridstack": "^1.11.3",
"@homarr/gridstack": "^1.12.0",
"@homarr/icons": "workspace:^0.1.0",
"@homarr/integrations": "workspace:^0.1.0",
"@homarr/log": "workspace:^",
"@homarr/modals": "workspace:^0.1.0",
@@ -39,18 +41,18 @@
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0",
"@mantine/colors-generator": "^7.16.0",
"@mantine/core": "^7.16.0",
"@mantine/dropzone": "^7.16.0",
"@mantine/hooks": "^7.16.0",
"@mantine/modals": "^7.16.0",
"@mantine/tiptap": "^7.16.0",
"@mantine/colors-generator": "^7.16.1",
"@mantine/core": "^7.16.1",
"@mantine/dropzone": "^7.16.1",
"@mantine/hooks": "^7.16.1",
"@mantine/modals": "^7.16.1",
"@mantine/tiptap": "^7.16.1",
"@million/lint": "1.0.14",
"@t3-oss/env-nextjs": "^0.11.1",
"@tabler/icons-react": "^3.28.1",
"@tanstack/react-query": "^5.64.1",
"@tanstack/react-query-devtools": "^5.64.1",
"@tanstack/react-query-next-experimental": "5.64.1",
"@tabler/icons-react": "^3.29.0",
"@tanstack/react-query": "^5.64.2",
"@tanstack/react-query-devtools": "^5.64.2",
"@tanstack/react-query-next-experimental": "5.64.2",
"@trpc/client": "next",
"@trpc/next": "next",
"@trpc/react-query": "next",
@@ -62,11 +64,11 @@
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
"dotenv": "^16.4.7",
"flag-icons": "^7.3.1",
"flag-icons": "^7.3.2",
"glob": "^11.0.1",
"jotai": "^2.11.0",
"jotai": "^2.11.1",
"mantine-react-table": "2.0.0-beta.8",
"next": "15.1.4",
"next": "15.1.6",
"postcss-preset-mantine": "^1.17.0",
"prismjs": "^1.29.0",
"react": "19.0.0",
@@ -94,6 +94,7 @@ export function TRPCReactProvider(props: PropsWithChildren) {
false: unstable_httpBatchStreamLink({
transformer: superjson,
url: getTrpcUrl(),
maxURLLength: 2083, // Suggested by tRPC: https://trpc.io/docs/client/links/httpBatchLink#setting-a-maximum-url-length
headers: createHeadersCallbackForSource("nextjs-react (json)"),
}),
}),
@@ -2,6 +2,7 @@
import type { MouseEvent } from "react";
import { useCallback, useEffect } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Group, Menu } from "@mantine/core";
import { useHotkeys } from "@mantine/hooks";
@@ -9,9 +10,11 @@ import {
IconBox,
IconBoxAlignTop,
IconChevronDown,
IconLayoutBoard,
IconPencil,
IconPencilOff,
IconPlus,
IconReplace,
IconResize,
IconSettings,
} from "@tabler/icons-react";
@@ -49,6 +52,8 @@ export const BoardContentHeaderActions = () => {
<HeaderButton href={`/boards/${board.name}/settings`}>
<IconSettings stroke={1.5} />
</HeaderButton>
<SelectBoardsMenu />
</>
);
};
@@ -151,6 +156,32 @@ const EditModeMenu = () => {
);
};
const SelectBoardsMenu = () => {
const { data: boards = [] } = clientApi.board.getAllBoards.useQuery();
return (
<Menu position="bottom-end" withArrow>
<Menu.Target>
<HeaderButton w="auto" px={4}>
<IconReplace stroke={1.5} />
</HeaderButton>
</Menu.Target>
<Menu.Dropdown style={{ transform: "translate(-7px, 0)" }}>
{boards.map((board) => (
<Menu.Item
key={board.id}
component={Link}
href={`/boards/${board.name}`}
leftSection={<IconLayoutBoard size={20} />}
>
{board.name}
</Menu.Item>
))}
</Menu.Dropdown>
</Menu>
);
};
const usePreventLeaveWithDirty = (isDirty: boolean) => {
const t = useI18n();
const { openConfirmModal } = useConfirmModal();
@@ -15,7 +15,9 @@ export const IntegrationCreateDropdownContent = () => {
const [search, setSearch] = useState("");
const filteredKinds = useMemo(() => {
return integrationKinds.filter((kind) => kind.includes(search.toLowerCase()));
return integrationKinds.filter((kind) =>
getIntegrationName(kind).toLowerCase().includes(search.toLowerCase().trim()),
);
}, [search]);
const handleSearch = React.useCallback(
@@ -29,6 +31,7 @@ export const IntegrationCreateDropdownContent = () => {
leftSection={<IconSearch stroke={1.5} size={20} />}
placeholder={t("integration.page.list.search")}
value={search}
data-autofocus
onChange={handleSearch}
/>
@@ -102,7 +102,15 @@ export default async function IntegrationsPage(props: IntegrationsPageProps) {
const IntegrationSelectMenu = ({ children }: PropsWithChildren) => {
return (
<Menu width={256} trapFocus position="bottom-end" withinPortal shadow="md" keepMounted={false}>
<Menu
width={256}
trapFocus
position="bottom-end"
withinPortal
shadow="md"
keepMounted={false}
withInitialFocusPlaceholder={false}
>
{children}
<MenuDropdown>
<IntegrationCreateDropdownContent />
@@ -153,7 +153,7 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
{
label: t("items.help.items.submitIssue"),
icon: IconBrandGithub,
href: "https://github.com/ajnart/homarr/issues/new/choose",
href: "https://github.com/homarr-labs/homarr/issues/new/choose",
external: true,
},
{
@@ -165,7 +165,7 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
{
label: t("items.help.items.sourceCode"),
icon: IconGitFork,
href: "https://github.com/ajnart/homarr",
href: "https://github.com/homarr-labs/homarr",
external: true,
},
],
@@ -15,6 +15,11 @@ export const CopyMedia = ({ media }: CopyMediaProps) => {
const url = typeof window !== "undefined" ? `${window.location.origin}/api/user-medias/${media.id}` : "";
// Clipboard only works on localhost or secure connections (https)
if (url.startsWith("http://") && !url.startsWith("http://localhost")) {
return null;
}
return (
<CopyButton value={url}>
{({ copy, copied }) => (
@@ -1,13 +1,28 @@
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import { Anchor, Group, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from "@mantine/core";
import {
ActionIcon,
Anchor,
Group,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Title,
Tooltip,
} from "@mantine/core";
import { IconExternalLink } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { humanFileSize } from "@homarr/common";
import type { inferSearchParamsFromSchema } from "@homarr/common/types";
import { createLocalImageUrl } from "@homarr/icons/local";
import { getI18n } from "@homarr/translation/server";
import { SearchInput, TablePagination, UserAvatar } from "@homarr/ui";
import { z } from "@homarr/validation";
@@ -91,13 +106,14 @@ interface RowProps {
const Row = async ({ media }: RowProps) => {
const session = await auth();
const t = await getI18n();
const canDelete = media.creatorId === session?.user.id || session?.user.permissions.includes("media-full-all");
return (
<TableTr>
<TableTd w={64}>
<Image
src={`/api/user-medias/${media.id}`}
src={createLocalImageUrl(media.id)}
alt={media.name}
width={64}
height={64}
@@ -121,6 +137,17 @@ const Row = async ({ media }: RowProps) => {
<TableTd w={64}>
<Group wrap="nowrap" gap="xs">
<CopyMedia media={media} />
<Tooltip label={t("media.action.open.label")} openDelay={500}>
<ActionIcon
component="a"
href={createLocalImageUrl(media.id)}
target="_blank"
color="gray"
variant="subtle"
>
<IconExternalLink size={16} stroke={1.5} />
</ActionIcon>
</Tooltip>
{canDelete && <DeleteMedia media={media} />}
</Group>
</TableTd>
@@ -59,10 +59,17 @@ export default async function CertificatesPage({ params }: CertificatesPageProps
{x509Certificates.map((cert) => (
<Card key={cert.x509.fingerprint} withBorder>
<Group wrap="nowrap">
<IconCertificate color={getMantineColor(iconColor(cert.x509.validToDate), 6)} size={32} stroke={1.5} />
<Stack flex={1} gap="xs">
<Group justify="space-between">
<Text fw={500}>{cert.x509.subject}</Text>
<IconCertificate
color={getMantineColor(iconColor(cert.x509.validToDate), 6)}
style={{ minWidth: 32 }}
size={32}
stroke={1.5}
/>
<Stack flex={1} gap="xs" maw="calc(100% - 48px)">
<Group justify="space-between" wrap="nowrap">
<Text fw={500} lineClamp={1} style={{ wordBreak: "break-all" }}>
{cert.x509.subject}
</Text>
<Text c="gray.6" ta="end" size="sm">
{cert.fileName}
</Text>
@@ -16,7 +16,7 @@ import { MantineReactTable } from "mantine-react-table";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useTimeAgo } from "@homarr/common";
import type { DockerContainerState } from "@homarr/definitions";
import type { ContainerState } from "@homarr/docker";
import { useModalAction } from "@homarr/modals";
import { AddDockerAppToHomarr } from "@homarr/modals-collection";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
@@ -246,9 +246,9 @@ const containerStates = {
exited: "red",
removing: "pink",
dead: "dark",
} satisfies Record<DockerContainerState, MantineColor>;
} satisfies Record<ContainerState, MantineColor>;
const ContainerStateBadge = ({ state }: { state: DockerContainerState }) => {
const ContainerStateBadge = ({ state }: { state: ContainerState }) => {
const t = useScopedI18n("docker.field.state.option");
return (
@@ -51,7 +51,10 @@ export const PingIconsEnabled = ({ user }: PingIconsEnabledProps) => {
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<Switch {...form.getInputProps("pingIconsEnabled")} label={t("user.field.pingIconsEnabled.label")} />
<Switch
{...form.getInputProps("pingIconsEnabled", { type: "checkbox" })}
label={t("user.field.pingIconsEnabled.label")}
/>
<Group justify="end">
<Button type="submit" color="teal" loading={isPending}>
@@ -83,10 +83,12 @@ export default async function EditUserPage(props: Props) {
</Box>
</Group>
<Stack mb="lg">
<Title order={2}>{tGeneral("item.language")}</Title>
<CurrentLanguageCombobox />
</Stack>
{session?.user.id === user.id && (
<Stack mb="lg">
<Title order={2}>{tGeneral("item.language")}</Title>
<CurrentLanguageCombobox />
</Stack>
)}
<Stack mb="lg">
<Title order={2}>{tGeneral("item.board.title")}</Title>
+3 -1
View File
@@ -30,7 +30,9 @@ const handler = auth(async (req) => {
req,
createContext: () => createTRPCContext({ session: req.auth, headers: req.headers }),
onError({ error, path, type }) {
logger.error(`tRPC Error with ${type} on '${path}': (${error.code}) - ${error.message}`);
logger.error(
`tRPC Error with ${type} on '${path}': (${error.code}) - ${error.message}\n${error.stack}\n${error.cause}`,
);
},
});
@@ -0,0 +1,62 @@
import type { Modify } from "@homarr/common/types";
import { createId } from "@homarr/db/client";
import type { WidgetKind } from "@homarr/definitions";
import type { Board, DynamicSection, EmptySection, Item } from "~/app/[locale]/boards/_types";
import { getFirstEmptyPosition } from "./empty-position";
export interface CreateItemInput {
kind: WidgetKind;
}
export const createItemCallback =
({ kind }: CreateItemInput) =>
(previous: Board): Board => {
const firstSection = previous.sections
.filter((section): section is EmptySection => section.kind === "empty")
.sort((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset)
.at(0);
if (!firstSection) return previous;
const dynamicSectionsOfFirstSection = previous.sections.filter(
(section): section is DynamicSection => section.kind === "dynamic" && section.parentSectionId === firstSection.id,
);
const elements = [...firstSection.items, ...dynamicSectionsOfFirstSection];
const emptyPosition = getFirstEmptyPosition(elements, previous.columnCount);
if (!emptyPosition) {
console.error("Your board is full");
return previous;
}
const widget = {
id: createId(),
kind,
options: {},
width: 1,
height: 1,
...emptyPosition,
integrationIds: [],
advancedOptions: {
customCssClasses: [],
},
} satisfies Modify<
Item,
{
kind: WidgetKind;
}
>;
return {
...previous,
sections: previous.sections.map((section) => {
// Return same section if item is not in it
if (section.id !== firstSection.id) return section;
return {
...section,
items: section.items.concat(widget),
};
}),
};
};
@@ -0,0 +1,81 @@
import { createId } from "@homarr/db/client";
import type { Board, DynamicSection, EmptySection } from "~/app/[locale]/boards/_types";
import { getFirstEmptyPosition } from "./empty-position";
export interface DuplicateItemInput {
itemId: string;
}
export const duplicateItemCallback =
({ itemId }: DuplicateItemInput) =>
(previous: Board): Board => {
const itemToDuplicate = previous.sections
.flatMap((section) => section.items.map((item) => ({ ...item, sectionId: section.id })))
.find((item) => item.id === itemId);
if (!itemToDuplicate) return previous;
const currentSection = previous.sections.find((section) => section.id === itemToDuplicate.sectionId);
if (!currentSection) return previous;
const dynamicSectionsOfCurrentSection = previous.sections.filter(
(section): section is DynamicSection =>
section.kind === "dynamic" && section.parentSectionId === currentSection.id,
);
const elements = [...currentSection.items, ...dynamicSectionsOfCurrentSection];
let sectionId = currentSection.id;
let emptyPosition = getFirstEmptyPosition(
elements,
currentSection.kind === "dynamic" ? currentSection.width : previous.columnCount,
currentSection.kind === "dynamic" ? currentSection.height : undefined,
{
width: itemToDuplicate.width,
height: itemToDuplicate.height,
},
);
if (!emptyPosition) {
const firstSection = previous.sections
.filter((section): section is EmptySection => section.kind === "empty")
.sort((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset)
.at(0);
if (!firstSection) return previous;
const dynamicSectionsOfFirstSection = previous.sections.filter(
(section): section is DynamicSection =>
section.kind === "dynamic" && section.parentSectionId === firstSection.id,
);
const elements = [...firstSection.items, ...dynamicSectionsOfFirstSection];
emptyPosition = getFirstEmptyPosition(elements, previous.columnCount, undefined, {
width: itemToDuplicate.width,
height: itemToDuplicate.height,
});
if (!emptyPosition) {
console.error("Your board is full");
return previous;
}
sectionId = firstSection.id;
}
const widget = structuredClone(itemToDuplicate);
widget.id = createId();
widget.xOffset = emptyPosition.xOffset;
widget.yOffset = emptyPosition.yOffset;
widget.sectionId = sectionId;
const result = {
...previous,
sections: previous.sections.map((section) => {
// Return same section if item is not in it
if (section.id !== sectionId) return section;
return {
...section,
items: section.items.concat(widget),
};
}),
};
return result;
};
@@ -0,0 +1,25 @@
import type { Item } from "~/app/[locale]/boards/_types";
export const getFirstEmptyPosition = (
elements: Pick<Item, "yOffset" | "xOffset" | "width" | "height">[],
columnCount: number,
rowCount = 9999,
size: { width: number; height: number } = { width: 1, height: 1 },
) => {
for (let yOffset = 0; yOffset < rowCount + 1 - size.height; yOffset++) {
for (let xOffset = 0; xOffset < columnCount; xOffset++) {
const isOccupied = elements.some(
(element) =>
element.yOffset < yOffset + size.height &&
element.yOffset + element.height > yOffset &&
element.xOffset < xOffset + size.width &&
element.xOffset + element.width > xOffset,
);
if (!isOccupied) {
return { xOffset, yOffset };
}
}
}
return undefined;
};
@@ -0,0 +1,61 @@
import { describe, expect, test, vi } from "vitest";
import type { Board } from "~/app/[locale]/boards/_types";
import { createItemCallback } from "../create-item";
import * as emptyPosition from "../empty-position";
import { createDynamicSection, createEmptySection, createItem } from "./shared";
describe("item actions create-item", () => {
test("should add it to first section", () => {
const spy = vi.spyOn(emptyPosition, "getFirstEmptyPosition");
spy.mockReturnValue({ xOffset: 5, yOffset: 5 });
const input = {
sections: [createEmptySection("1", 2), createEmptySection("2", 0), createEmptySection("3", 1)],
columnCount: 4,
} satisfies Pick<Board, "sections" | "columnCount">;
const result = createItemCallback({
kind: "clock",
})(input as unknown as Board);
const firstSection = result.sections.find((section) => section.id === "2");
const item = firstSection?.items.at(0);
expect(item).toEqual(expect.objectContaining({ kind: "clock", xOffset: 5, yOffset: 5 }));
expect(spy).toHaveBeenCalledWith([], input.columnCount);
});
test("should correctly pass dynamic section and items to getFirstEmptyPosition", () => {
const spy = vi.spyOn(emptyPosition, "getFirstEmptyPosition");
spy.mockReturnValue({ xOffset: 5, yOffset: 5 });
const firstSection = createEmptySection("2", 0);
const expectedItem = createItem({ id: "12", xOffset: 1, yOffset: 2, width: 3, height: 2 });
firstSection.items.push(expectedItem);
const dynamicSectionInFirst = createDynamicSection({
id: "4",
parentSectionId: "2",
yOffset: 0,
xOffset: 0,
width: 2,
height: 2,
});
const input = {
sections: [
createEmptySection("1", 2),
firstSection,
createEmptySection("3", 1),
dynamicSectionInFirst,
createDynamicSection({ id: "5", parentSectionId: "3", yOffset: 1 }),
],
columnCount: 4,
} satisfies Pick<Board, "sections" | "columnCount">;
const result = createItemCallback({
kind: "clock",
})(input as unknown as Board);
const firstSectionResult = result.sections.find((section) => section.id === "2");
const item = firstSectionResult?.items.find((item) => item.id !== "12");
expect(item).toEqual(expect.objectContaining({ kind: "clock", xOffset: 5, yOffset: 5 }));
expect(spy).toHaveBeenCalledWith([expectedItem, dynamicSectionInFirst], input.columnCount);
});
});
@@ -0,0 +1,52 @@
import { describe, expect, test, vi } from "vitest";
import type { Board } from "~/app/[locale]/boards/_types";
import { duplicateItemCallback } from "../duplicate-item";
import * as emptyPosition from "../empty-position";
import { createEmptySection, createItem } from "./shared";
describe("item actions duplicate-item", () => {
test("should copy it in the same section", () => {
const spy = vi.spyOn(emptyPosition, "getFirstEmptyPosition");
spy.mockReturnValue({ xOffset: 5, yOffset: 5 });
const currentSection = createEmptySection("2", 1);
const currentItem = createItem({
id: "1",
xOffset: 1,
yOffset: 3,
width: 3,
height: 2,
kind: "minecraftServerStatus",
integrationIds: ["1"],
options: { address: "localhost" },
advancedOptions: { customCssClasses: ["test"] },
});
const otherItem = createItem({
id: "2",
});
currentSection.items.push(currentItem, otherItem);
const input = {
columnCount: 10,
sections: [createEmptySection("1", 0), currentSection, createEmptySection("3", 2)],
} satisfies Pick<Board, "sections" | "columnCount">;
const result = duplicateItemCallback({ itemId: currentItem.id })(input as unknown as Board);
const section = result.sections.find((section) => section.id === "2");
expect(section?.items.length).toBe(3);
const duplicatedItem = section?.items.find((item) => item.id !== currentItem.id && item.id !== otherItem.id);
expect(duplicatedItem).toEqual(
expect.objectContaining({
kind: "minecraftServerStatus",
xOffset: 5,
yOffset: 5,
width: 3,
height: 2,
integrationIds: ["1"],
options: { address: "localhost" },
advancedOptions: { customCssClasses: ["test"] },
}),
);
});
});
@@ -0,0 +1,138 @@
import { describe, expect, test } from "vitest";
import type { Item } from "~/app/[locale]/boards/_types";
import { getFirstEmptyPosition } from "../empty-position";
describe("get first empty position", () => {
test.each([
[[[" ", " ", " ", " "]], [1, 1], 0, 0],
[[["a", " ", " ", " "]], [1, 1], 1, 0],
[[[" ", "a", " ", " "]], [1, 1], 0, 0],
[
[
["a", "a", " ", " "],
["a", "a", " ", " "],
],
[1, 1],
2,
0,
],
[[["a", "a", "a", "a"]], [1, 1], 0, 1],
[
[
["a", "a", "a", "a"],
["a", "a", "a", "a"],
],
[1, 1],
0,
2,
],
[
[
["a", "a", " ", "b", "b"],
["a", "a", " ", "b", "b"],
],
[1, 2],
2,
0,
],
[
[
["a", "a", " ", " ", "b", "b"],
["a", "a", " ", " ", "b", "b"],
],
[2, 2],
2,
0,
],
[
[
["a", "a", " ", "d", "b", "b"],
["a", "a", "c", "e", "b", "b"],
],
[1, 1],
2,
0,
],
[
[
["a", "a", " ", " ", "b", "b"],
["a", "a", " ", "e", "b", "b"],
],
[2, 2],
0,
2,
],
])("should return the first empty position", (layout, size, expectedX, expectedY) => {
const elements = createElementsFromLayout(layout);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const result = getFirstEmptyPosition(elements, layout[0]!.length, undefined, { width: size[0]!, height: size[1]! });
expect(result).toEqual({ xOffset: expectedX, yOffset: expectedY });
});
test.each([
[[[" ", " "]], [1, 1], 0, 0, 1],
[[["a", " "]], [1, 1], 1, 0, 1],
[[["a", "a"]], [1, 1], undefined, undefined, 1],
[[["a", "a"]], [1, 1], 0, 1, 2],
[
[
["a", "b", " ", " "],
["a", "c", " ", "d"],
],
[2, 2],
undefined,
undefined,
3,
],
[
[
["a", "b", " ", " "],
["a", "c", " ", "d"],
],
[2, 2],
0,
2,
4,
],
[[["a", "b"]], [2, 1], 0, 1, 2],
])("should return the first empty position with limited rows", (layout, size, expectedX, expectedY, rowCount) => {
const elements = createElementsFromLayout(layout);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const result = getFirstEmptyPosition(elements, layout[0]!.length, rowCount, { width: size[0]!, height: size[1]! });
expect(result).toEqual(expectedX !== undefined ? { xOffset: expectedX, yOffset: expectedY } : undefined);
});
});
const createElementsFromLayout = (layout: string[][]) => {
const elements: (Pick<Item, "xOffset" | "yOffset" | "width" | "height"> & { char: string })[] = [];
for (let yOffset = 0; yOffset < layout.length; yOffset++) {
const row = layout[yOffset];
if (!row) continue;
for (let xOffset = 0; xOffset < row.length; xOffset++) {
const item = row[xOffset];
if (item === " " || !item) continue;
const existing = elements.find((element) => element.char === item);
if (existing) {
existing.height = yOffset - existing.yOffset + 1;
existing.width = xOffset - existing.xOffset + 1;
continue;
}
elements.push({
yOffset,
xOffset,
width: 1,
height: 1,
char: item,
});
}
}
return elements;
};
@@ -0,0 +1,32 @@
import type { DynamicSection, EmptySection, Item } from "~/app/[locale]/boards/_types";
export const createEmptySection = (id: string, yOffset: number): EmptySection => ({
id,
kind: "empty",
yOffset,
xOffset: 0,
items: [],
});
export const createDynamicSection = (section: Omit<Partial<DynamicSection>, "kind">): DynamicSection => ({
id: section.id ?? "0",
kind: "dynamic",
parentSectionId: section.parentSectionId ?? "0",
height: section.height ?? 1,
width: section.width ?? 1,
yOffset: section.yOffset ?? 0,
xOffset: section.xOffset ?? 0,
items: section.items ?? [],
});
export const createItem = (item: Partial<Item>): Item => ({
id: item.id ?? "0",
width: item.width ?? 1,
height: item.height ?? 1,
yOffset: item.yOffset ?? 0,
xOffset: item.xOffset ?? 0,
kind: item.kind ?? "clock",
integrationIds: item.integrationIds ?? [],
options: item.options ?? {},
advancedOptions: item.advancedOptions ?? { customCssClasses: [] },
});
@@ -1,12 +1,13 @@
import { useCallback } from "react";
import type { Modify } from "@homarr/common/types";
import { createId } from "@homarr/db/client";
import type { WidgetKind } from "@homarr/definitions";
import type { BoardItemAdvancedOptions } from "@homarr/validation";
import type { EmptySection, Item } from "~/app/[locale]/boards/_types";
import type { Item } from "~/app/[locale]/boards/_types";
import { useUpdateBoard } from "~/app/[locale]/boards/(content)/_client";
import type { CreateItemInput } from "./actions/create-item";
import { createItemCallback } from "./actions/create-item";
import type { DuplicateItemInput } from "./actions/duplicate-item";
import { duplicateItemCallback } from "./actions/duplicate-item";
interface MoveAndResizeItem {
itemId: string;
@@ -42,87 +43,19 @@ interface UpdateItemIntegrations {
newIntegrations: string[];
}
interface CreateItem {
kind: WidgetKind;
}
interface DuplicateItem {
itemId: string;
}
export const useItemActions = () => {
const { updateBoard } = useUpdateBoard();
const createItem = useCallback(
({ kind }: CreateItem) => {
updateBoard((previous) => {
const lastSection = previous.sections
.filter((section): section is EmptySection => section.kind === "empty")
.sort((sectionA, sectionB) => sectionB.yOffset - sectionA.yOffset)[0];
if (!lastSection) return previous;
const widget = {
id: createId(),
kind,
options: {},
width: 1,
height: 1,
integrationIds: [],
advancedOptions: {
customCssClasses: [],
},
} satisfies Modify<
Omit<Item, "yOffset" | "xOffset">,
{
kind: WidgetKind;
}
>;
return {
...previous,
sections: previous.sections.map((section) => {
// Return same section if item is not in it
if (section.id !== lastSection.id) return section;
return {
...section,
items: section.items.concat(widget as unknown as Item),
};
}),
};
});
(input: CreateItemInput) => {
updateBoard(createItemCallback(input));
},
[updateBoard],
);
const duplicateItem = useCallback(
({ itemId }: DuplicateItem) => {
updateBoard((previous) => {
const itemToDuplicate = previous.sections
.flatMap((section) => section.items)
.find((item) => item.id === itemId);
if (!itemToDuplicate) return previous;
const newItem = {
...itemToDuplicate,
id: createId(),
yOffset: undefined,
xOffset: undefined,
} satisfies Modify<Item, { yOffset?: number; xOffset?: number }>;
return {
...previous,
sections: previous.sections.map((section) => {
// Return same section if item is not in it
if (!section.items.some((item) => item.id === itemId)) return section;
return {
...section,
items: section.items.concat(newItem as unknown as Item),
};
}),
};
});
({ itemId }: DuplicateItemInput) => {
updateBoard(duplicateItemCallback({ itemId }));
},
[updateBoard],
);
@@ -79,7 +79,12 @@ const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
>
<Throw
error={new NoIntegrationSelectedError()}
when={widgetSupportsIntegrations && item.integrationIds.length === 0}
when={
widgetSupportsIntegrations &&
item.integrationIds.length === 0 &&
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
(!("integrationsRequired" in definition) || definition.integrationsRequired !== false)
}
/>
<BoardItemMenu offset={4} item={newItem} />
<Comp
+2 -7
View File
@@ -12,11 +12,7 @@ export const env = createEnv({
* Specify your server-side environment variables schema here. This way you can ensure the app isn't
* built with invalid env vars.
*/
server: {
// Comma separated list of docker hostnames that can be used to connect to query the docker endpoints (localhost:2375,host.docker.internal:2375, ...)
DOCKER_HOSTNAMES: z.string().optional(),
DOCKER_PORTS: z.string().optional(),
},
server: {},
/**
* Specify your client-side environment variables schema here.
* For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`.
@@ -30,9 +26,8 @@ export const env = createEnv({
runtimeEnv: {
PORT: process.env.PORT,
NODE_ENV: process.env.NODE_ENV,
DOCKER_HOSTNAMES: process.env.DOCKER_HOSTNAMES,
DOCKER_PORTS: process.env.DOCKER_PORTS,
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
},
skipValidation: shouldSkipEnvValidation(),
emptyStringAsUndefined: true,
});
+1 -1
View File
@@ -38,7 +38,7 @@
"dayjs": "^1.11.13",
"dotenv": "^16.4.7",
"superjson": "2.2.2",
"undici": "7.2.3"
"undici": "7.3.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+3 -10
View File
@@ -24,10 +24,10 @@
<strong>Demo ✨</strong>
</a>
•
<a href="https://homarr.dev/docs/getting-started/installation/">
<a href="https://homarr.dev/docs/category/installation-1/">
<strong>Install 💻</strong>
</a> •
<a href="https://translate.homarr.dev/">
<a href="https://crowdin.com/project/homarr_labs">
<strong>Translations 🈺</strong>
</a> •
<a href="https://discord.com/invite/aCsmEV5RgA">
@@ -35,13 +35,6 @@
</a>
</p>
> [!WARNING]
> This is a beta version of Homarr. You can download the old but stable version at https://github.com/ajnart/homarr/. Running this version is at your own risk.
Simplify the management of your server with Homarr - a sleek, modern dashboard that puts all of your apps and services at your fingertips. With Homarr, you can access and control everything in one location. Homarr seamlessly integrates with the apps you've added, providing you with valuable information and giving you complete control. Installation is a breeze, and Homarr supports a wide range of deployment methods.
<br/>
<br/>
![Features Section](./section-features.png)
@@ -100,7 +93,7 @@ Homarr has a [built-in collection of widgets and integrations](https://homarr.de
Since we are updating Homarr very frequently, we recommend reading our official installation guides:
<h2>
<a href="https://homarr.dev/docs/getting-started/installation/">
<a href="https://homarr.dev/docs/category/installation-1/">
Please click here for official installation instructions
</a>
</h2>
+3 -3
View File
@@ -49,9 +49,9 @@ describe("Onboarding", () => {
environment: {
AUTH_PROVIDERS: "ldap",
AUTH_LDAP_URI: "ldap://host.docker.internal:3890",
AUTH_LDAP_BASE: "",
AUTH_LDAP_BIND_DN: "",
AUTH_LDAP_BIND_PASSWORD: "",
AUTH_LDAP_BASE: "not-used",
AUTH_LDAP_BIND_DN: "not-used",
AUTH_LDAP_BIND_PASSWORD: "not-used",
},
mounts: {
"/appdata": localMountPath,
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "1.0.0",
"version": "1.1.0",
"private": true,
"scripts": {
"build": "cross-env CI=true turbo build",
@@ -40,27 +40,27 @@
"@semantic-release/release-notes-generator": "^14.0.3",
"@turbo/gen": "^2.3.3",
"@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "^2.1.8",
"@vitest/ui": "^2.1.8",
"@vitest/coverage-v8": "^3.0.3",
"@vitest/ui": "^3.0.3",
"conventional-changelog-conventionalcommits": "^8.0.0",
"cross-env": "^7.0.3",
"jsdom": "^26.0.0",
"prettier": "^3.4.2",
"semantic-release": "^24.2.1",
"testcontainers": "^10.16.0",
"testcontainers": "^10.17.1",
"turbo": "^2.3.3",
"typescript": "^5.7.3",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^2.1.8"
"vitest": "^3.0.3"
},
"packageManager": "pnpm@9.15.4",
"engines": {
"node": ">=22.13.0"
"node": ">=22.13.1"
},
"pnpm": {
"allowNonAppliedPatches": true,
"overrides": {
"proxmox-api>undici": "7.2.3"
"proxmox-api>undici": "7.3.0"
},
"patchedDependencies": {
"pretty-print-error": "patches/pretty-print-error.patch"
+2 -3
View File
@@ -29,6 +29,7 @@
"@homarr/cron-jobs": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/docker": "workspace:^0.1.0",
"@homarr/icons": "workspace:^0.1.0",
"@homarr/integrations": "workspace:^0.1.0",
"@homarr/log": "workspace:^",
@@ -42,9 +43,8 @@
"@trpc/client": "next",
"@trpc/react-query": "next",
"@trpc/server": "next",
"dockerode": "4.0.2",
"lodash.clonedeep": "^4.5.0",
"next": "15.1.4",
"next": "15.1.6",
"react": "19.0.0",
"react-dom": "19.0.0",
"superjson": "2.2.2",
@@ -54,7 +54,6 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/dockerode": "^3.3.34",
"eslint": "^9.18.0",
"prettier": "^3.4.2",
"typescript": "^5.7.3"
+41 -38
View File
@@ -98,56 +98,59 @@ export const createManyIntegrationMiddleware = <TKind extends IntegrationKind>(
action: IntegrationAction,
...kinds: AtLeastOneOf<TKind> // Ensure at least one kind is provided
) => {
return publicProcedure
.input(z.object({ integrationIds: z.array(z.string()).min(1) }))
.use(async ({ ctx, input, next }) => {
const dbIntegrations = await ctx.db.query.integrations.findMany({
where: and(inArray(integrations.id, input.integrationIds), inArray(integrations.kind, kinds)),
with: {
secrets: true,
items: {
return publicProcedure.input(z.object({ integrationIds: z.array(z.string()) })).use(async ({ ctx, input, next }) => {
const dbIntegrations =
input.integrationIds.length >= 1
? await ctx.db.query.integrations.findMany({
where: and(inArray(integrations.id, input.integrationIds), inArray(integrations.kind, kinds)),
with: {
item: {
secrets: true,
items: {
with: {
section: {
columns: {
boardId: true,
item: {
with: {
section: {
columns: {
boardId: true,
},
},
},
},
},
},
userPermissions: true,
groupPermissions: true,
},
},
userPermissions: true,
groupPermissions: true,
},
})
: [];
const offset = input.integrationIds.length - dbIntegrations.length;
if (offset !== 0) {
throw new TRPCError({
code: "NOT_FOUND",
message: `${offset} of the specified integrations not found or not of kinds ${kinds.join(",")}: ([${input.integrationIds.join(",")}] compared to [${dbIntegrations.map(({ id, kind }) => `${kind}:${id}`).join(",")}])`,
});
}
const offset = input.integrationIds.length - dbIntegrations.length;
if (offset !== 0) {
throw new TRPCError({
code: "NOT_FOUND",
message: `${offset} of the specified integrations not found or not of kinds ${kinds.join(",")}: ([${input.integrationIds.join(",")}] compared to [${dbIntegrations.map(({ id, kind }) => `${kind}:${id}`).join(",")}])`,
});
}
if (dbIntegrations.length >= 1) {
await throwIfActionIsNotAllowedAsync(action, ctx.db, dbIntegrations, ctx.session);
}
return next({
ctx: {
integrations: dbIntegrations.map(
({ secrets, kind, items: _ignore1, groupPermissions: _ignore2, userPermissions: _ignore3, ...rest }) => ({
...rest,
kind: kind as TKind,
decryptedSecrets: secrets.map((secret) => ({
...secret,
value: decryptSecret(secret.value),
})),
}),
),
},
});
return next({
ctx: {
integrations: dbIntegrations.map(
({ secrets, kind, items: _ignore1, groupPermissions: _ignore2, userPermissions: _ignore3, ...rest }) => ({
...rest,
kind: kind as TKind,
decryptedSecrets: secrets.map((secret) => ({
...secret,
value: decryptSecret(secret.value),
})),
}),
),
},
});
});
};
/**
+10 -13
View File
@@ -1,26 +1,24 @@
import { TRPCError } from "@trpc/server";
import type Docker from "dockerode";
import type { Container } from "dockerode";
import { db, like, or } from "@homarr/db";
import { icons } from "@homarr/db/schema";
import type { DockerContainerState } from "@homarr/definitions";
import { DockerSingleton } from "@homarr/docker";
import type { Container, ContainerInfo, ContainerState, Docker, Port } from "@homarr/docker";
import { logger } from "@homarr/log";
import { createCacheChannel } from "@homarr/redis";
import { z } from "@homarr/validation";
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
import { DockerSingleton } from "./docker-singleton";
const dockerCache = createCacheChannel<{
containers: (Docker.ContainerInfo & { instance: string; iconUrl: string | null })[];
containers: (ContainerInfo & { instance: string; iconUrl: string | null })[];
}>("docker-containers", 5 * 60 * 1000);
export const dockerRouter = createTRPCRouter({
getContainers: permissionRequiredProcedure.requiresPermission("admin").query(async () => {
const result = await dockerCache
.consumeAsync(async () => {
const dockerInstances = DockerSingleton.getInstance();
const dockerInstances = DockerSingleton.getInstances();
const containers = await Promise.all(
// Return all the containers of all the instances into only one item
dockerInstances.map(({ instance, host: key }) =>
@@ -33,8 +31,7 @@ export const dockerRouter = createTRPCRouter({
),
).then((containers) => containers.flat());
const extractImage = (container: Docker.ContainerInfo) =>
container.Image.split("/").at(-1)?.split(":").at(0) ?? "";
const extractImage = (container: ContainerInfo) => container.Image.split("/").at(-1)?.split(":").at(0) ?? "";
const likeQueries = containers.map((container) => like(icons.name, `%${extractImage(container)}%`));
const dbIcons =
likeQueries.length >= 1
@@ -151,7 +148,7 @@ const getContainerOrDefaultAsync = async (instance: Docker, id: string) => {
};
const getContainerOrThrowAsync = async (id: string) => {
const dockerInstances = DockerSingleton.getInstance();
const dockerInstances = DockerSingleton.getInstances();
const containers = await Promise.all(dockerInstances.map(({ instance }) => getContainerOrDefaultAsync(instance, id)));
const foundContainer = containers.find((container) => container) ?? null;
@@ -168,21 +165,21 @@ const getContainerOrThrowAsync = async (id: string) => {
interface DockerContainer {
name: string;
id: string;
state: DockerContainerState;
state: ContainerState;
image: string;
ports: Docker.Port[];
ports: Port[];
iconUrl: string | null;
}
function sanitizeContainers(
containers: (Docker.ContainerInfo & { instance: string; iconUrl: string | null })[],
containers: (ContainerInfo & { instance: string; iconUrl: string | null })[],
): DockerContainer[] {
return containers.map((container) => {
return {
name: container.Names[0]?.split("/")[1] ?? "Unknown",
id: container.Id,
instance: container.instance,
state: container.State as DockerContainerState,
state: container.State as ContainerState,
image: container.Image,
ports: container.Ports,
iconUrl: container.iconUrl,
@@ -1,51 +0,0 @@
import Docker from "dockerode";
interface DockerInstance {
host: string;
instance: Docker;
}
export class DockerSingleton {
private static instances: DockerInstance[];
private createInstances() {
const instances: DockerInstance[] = [];
const hostVariable = process.env.DOCKER_HOST;
const portVariable = process.env.DOCKER_PORT;
if (hostVariable === undefined || portVariable === undefined) {
instances.push({ host: "socket", instance: new Docker() });
return instances;
}
const hosts = hostVariable.split(",");
const ports = portVariable.split(",");
if (hosts.length !== ports.length) {
throw new Error("The number of hosts and ports must match");
}
hosts.forEach((host, i) => {
instances.push({
host: `${host}:${ports[i]}`,
instance: new Docker({
host,
port: parseInt(ports[i] ?? "", 10),
}),
});
return instances;
});
return instances;
}
public static findInstance(key: string): DockerInstance | undefined {
return this.instances.find((instance) => instance.host === key);
}
public static getInstance(): DockerInstance[] {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!DockerSingleton.instances) {
DockerSingleton.instances = new DockerSingleton().createInstances();
}
return this.instances;
}
}
+7 -1
View File
@@ -404,7 +404,13 @@ export const userRouter = createTRPCRouter({
});
}
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.userId), "view");
// Only allow user to select boards they have access to
if (input.homeBoardId) {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.homeBoardId), "view");
}
if (input.mobileHomeBoardId) {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.mobileHomeBoardId), "view");
}
await ctx.db
.update(users)
+31 -7
View File
@@ -1,12 +1,36 @@
import type { RssFeed } from "@homarr/cron-jobs";
import { createItemChannel } from "@homarr/redis";
import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds";
import { z } from "@homarr/validation";
import { createOneItemMiddleware } from "../../middlewares/item";
import { createTRPCRouter, publicProcedure } from "../../trpc";
export const rssFeedRouter = createTRPCRouter({
getFeeds: publicProcedure.unstable_concat(createOneItemMiddleware("rssFeed")).query(async ({ input }) => {
const channel = createItemChannel<RssFeed[]>(input.itemId);
return await channel.getAsync();
}),
getFeeds: publicProcedure
.input(
z.object({
urls: z.array(z.string()),
maximumAmountPosts: z.number(),
}),
)
.query(async ({ input }) => {
const rssFeeds = await Promise.all(
input.urls.map(async (url) => {
const innerHandler = rssFeedsRequestHandler.handler({
url,
count: input.maximumAmountPosts,
});
return await innerHandler.getCachedOrUpdatedDataAsync({
forceUpdate: false,
});
}),
);
return rssFeeds
.flatMap((rssFeed) => rssFeed.data.entries)
.slice(0, input.maximumAmountPosts)
.sort((entryA, entryB) => {
return entryA.published && entryB.published
? new Date(entryB.published).getTime() - new Date(entryA.published).getTime()
: 0;
});
}),
});
+7 -4
View File
@@ -2,9 +2,11 @@ import type { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapte
import { cookies } from "next/headers";
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { formatError } from "pretty-print-error";
import { db } from "@homarr/db";
import type { SupportedAuthProvider } from "@homarr/definitions";
import { logger } from "@homarr/log";
import { createAdapter } from "./adapter";
import { createSessionCallback } from "./callbacks";
@@ -26,15 +28,16 @@ export const createConfiguration = (
const adapter = createAdapter(db, provider);
return NextAuth({
logger: {
error: (code, ...message) => {
error: (error) => {
// Remove the big error message for failed login attempts
// as it is not useful for the user.
if (code.name === "CredentialsSignin") {
console.warn("The login attempt of a user was not successful.");
if (error.name === "CredentialsSignin") {
logger.warn("The login attempt of a user was not successful.");
return;
}
console.error(code, ...message);
logger.error(formatError(error));
logger.error(formatError(error.cause));
},
},
trustHost: true,
+1
View File
@@ -86,4 +86,5 @@ export const env = createEnv({
AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE: process.env.AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE,
},
skipValidation,
emptyStringAsUndefined: true,
});
+2 -1
View File
@@ -34,8 +34,9 @@
"bcrypt": "^5.1.1",
"cookies": "^0.9.1",
"ldapts": "7.3.1",
"next": "15.1.4",
"next": "15.1.6",
"next-auth": "5.0.0-beta.25",
"pretty-print-error": "^1.1.2",
"react": "19.0.0",
"react-dom": "19.0.0"
},
@@ -1,5 +1,6 @@
import { CredentialsSignin } from "@auth/core/errors";
import { extractErrorMessage } from "@homarr/common";
import type { Database, InferInsertModel } from "@homarr/db";
import { and, createId, eq } from "@homarr/db";
import { users } from "@homarr/db/schema";
@@ -21,8 +22,8 @@ export const authorizeWithLdapCredentialsAsync = async (
distinguishedName: env.AUTH_LDAP_BIND_DN,
password: env.AUTH_LDAP_BIND_PASSWORD,
})
.catch(() => {
logger.error("Failed to connect to LDAP server");
.catch((error) => {
logger.error(`Failed to connect to LDAP server ${extractErrorMessage(error)}`);
throw new CredentialsSignin();
});
+1 -1
View File
@@ -23,7 +23,7 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"undici": "7.2.3"
"undici": "7.3.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+1
View File
@@ -26,4 +26,5 @@ export const env = createEnv({
SECRET_ENCRYPTION_KEY: process.env.SECRET_ENCRYPTION_KEY,
},
skipValidation: shouldSkipEnvValidation(),
emptyStringAsUndefined: true,
});
+2 -2
View File
@@ -29,10 +29,10 @@
"dependencies": {
"@homarr/log": "workspace:^0.1.0",
"dayjs": "^1.11.13",
"next": "15.1.4",
"next": "15.1.6",
"react": "19.0.0",
"react-dom": "19.0.0",
"undici": "7.2.3",
"undici": "7.3.0",
"zod": "^3.24.1"
},
"devDependencies": {
-1
View File
@@ -22,7 +22,6 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@extractus/feed-extractor": "^7.1.3",
"@homarr/analytics": "workspace:^0.1.0",
"@homarr/auth": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
-2
View File
@@ -11,7 +11,6 @@ import { mediaServerJob } from "./jobs/integrations/media-server";
import { mediaTranscodingJob } from "./jobs/integrations/media-transcoding";
import { minecraftServerStatusJob } from "./jobs/minecraft-server-status";
import { pingJob } from "./jobs/ping";
import type { RssFeed } from "./jobs/rss-feeds";
import { rssFeedsJob } from "./jobs/rss-feeds";
import { sessionCleanupJob } from "./jobs/session-cleanup";
import { updateCheckerJob } from "./jobs/update-checker";
@@ -38,4 +37,3 @@ export const jobGroup = createCronJobGroup({
});
export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
export type { RssFeed };
+19 -122
View File
@@ -1,139 +1,36 @@
import type { FeedData, FeedEntry } from "@extractus/feed-extractor";
import { extract } from "@extractus/feed-extractor";
import SuperJSON from "superjson";
import type { Modify } from "@homarr/common/types";
import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions";
import { db, eq } from "@homarr/db";
import { items } from "@homarr/db/schema";
import { logger } from "@homarr/log";
import { createItemChannel } from "@homarr/redis";
import { z } from "@homarr/validation";
// This import is done that way to avoid circular dependencies.
import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds";
import type { WidgetComponentProps } from "../../../widgets";
import { createCronJob } from "../lib";
export const rssFeedsJob = createCronJob("rssFeeds", EVERY_5_MINUTES).withCallback(async () => {
const itemsForIntegration = await db.query.items.findMany({
const rssItems = await db.query.items.findMany({
where: eq(items.kind, "rssFeed"),
});
for (const item of itemsForIntegration) {
const options = SuperJSON.parse<WidgetComponentProps<"rssFeed">["options"]>(item.options);
const itemOptions = rssItems.map((item) => SuperJSON.parse<WidgetComponentProps<"rssFeed">["options"]>(item.options));
const feeds = await Promise.all(
options.feedUrls.map(async (feedUrl) => ({
feedUrl,
feed: (await extract(feedUrl, {
getExtraEntryFields: (feedEntry) => {
const media = attemptGetImageFromEntry(feedUrl, feedEntry);
if (!media) {
return {};
}
return {
enclosure: media,
};
},
})) as ExtendedFeedData,
})),
);
const channel = createItemChannel<RssFeed[]>(item.id);
await channel.publishAndUpdateLastStateAsync(feeds);
for (const option of itemOptions) {
const maxAmountPosts = typeof option.maximumAmountPosts === "number" ? option.maximumAmountPosts : 100;
for (const url of option.feedUrls) {
try {
const innerHandler = rssFeedsRequestHandler.handler({
url,
count: maxAmountPosts,
});
await innerHandler.getCachedOrUpdatedDataAsync({
forceUpdate: true,
});
} catch (error) {
logger.error("Failed to update RSS feed", { url, error });
}
}
}
});
const attemptGetImageFromEntry = (feedUrl: string, entry: object) => {
const media = getFirstMediaProperty(entry);
if (media !== null) {
return media;
}
return getImageFromStringAsFallback(feedUrl, JSON.stringify(entry));
};
const getImageFromStringAsFallback = (feedUrl: string, content: string) => {
const regex = /https?:\/\/\S+?\.(jpg|jpeg|png|gif|bmp|svg|webp|tiff)/i;
const result = regex.exec(content);
if (result == null) {
return null;
}
console.debug(
`Falling back to regex image search for '${feedUrl}'. Found ${result.length} matches in content: ${content}`,
);
return result[0];
};
const mediaProperties = [
{
path: ["enclosure", "@_url"],
},
{
path: ["media:content", "@_url"],
},
];
/**
* The RSS and Atom standards are poorly adhered to in most of the web.
* We want to show pretty background images on the posts and therefore need to extract
* the enclosure (aka. media images). This function uses the dynamic properties defined above
* to search through the possible paths and detect valid image URLs.
* @param feedObject The object to scan for.
* @returns the value of the first path that is found within the object
*/
const getFirstMediaProperty = (feedObject: object) => {
for (const mediaProperty of mediaProperties) {
let propertyIndex = 0;
let objectAtPath: object = feedObject;
while (propertyIndex < mediaProperty.path.length) {
const key = mediaProperty.path[propertyIndex];
if (key === undefined) {
break;
}
const propertyEntries = Object.entries(objectAtPath);
const propertyEntry = propertyEntries.find(([entryKey]) => entryKey === key);
if (!propertyEntry) {
break;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const [_, propertyEntryValue] = propertyEntry;
objectAtPath = propertyEntryValue as object;
propertyIndex++;
}
const validationResult = z.string().url().safeParse(objectAtPath);
if (!validationResult.success) {
continue;
}
logger.debug(`Found an image in the feed entry: ${validationResult.data}`);
return validationResult.data;
}
return null;
};
/**
* We extend the feed with custom properties.
* This interface adds properties on top of the default ones.
*/
interface ExtendedFeedEntry extends FeedEntry {
enclosure?: string;
}
/**
* We extend the feed with custom properties.
* This interface omits the default entries with our custom definition.
*/
type ExtendedFeedData = Modify<
FeedData,
{
entries?: ExtendedFeedEntry;
}
>;
export interface RssFeed {
feedUrl: string;
feed: ExtendedFeedData;
}
+6 -1
View File
@@ -27,7 +27,11 @@ export const env = createEnv({
.default(drivers.betterSqlite3),
...(urlRequired
? {
DB_URL: z.string(),
DB_URL:
// Fallback to the default sqlite file path in production
process.env.NODE_ENV === "production" && isDriver("better-sqlite3")
? z.string().default("/appdata/db/db.sqlite")
: z.string().nonempty(),
}
: {}),
...(hostRequired
@@ -58,4 +62,5 @@ export const env = createEnv({
DB_PORT: process.env.DB_PORT,
},
skipValidation: shouldSkipEnvValidation(),
emptyStringAsUndefined: true,
});
+2 -2
View File
@@ -44,8 +44,8 @@
"@homarr/server-settings": "workspace:^0.1.0",
"@paralleldrive/cuid2": "^2.2.2",
"@t3-oss/env-nextjs": "^0.11.1",
"@testcontainers/mysql": "^10.16.0",
"better-sqlite3": "^11.8.0",
"@testcontainers/mysql": "^10.17.1",
"better-sqlite3": "^11.8.1",
"dotenv": "^16.4.7",
"drizzle-kit": "^0.30.2",
"drizzle-orm": "^0.38.4",
@@ -10,6 +10,7 @@ export type HomarrDocumentationPath =
| "/blog/2024/09/23/version-1.0"
| "/blog/2024/12/17/open-beta-1.0"
| "/blog/2024/12/31/migrate-secret-enryption-key"
| "/blog/2025/01/19/migration-guide-1.0"
| "/blog/archive"
| "/blog/authors"
| "/blog/authors/ajnart"
@@ -51,6 +52,7 @@ export type HomarrDocumentationPath =
| "/docs/tags/bookmark"
| "/docs/tags/bookmarks"
| "/docs/tags/caddy"
| "/docs/tags/certificates"
| "/docs/tags/checklist"
| "/docs/tags/code"
| "/docs/tags/community"
@@ -113,6 +115,7 @@ export type HomarrDocumentationPath =
| "/docs/tags/search"
| "/docs/tags/search-engines"
| "/docs/tags/security"
| "/docs/tags/self-signed"
| "/docs/tags/seo"
| "/docs/tags/server"
| "/docs/tags/settings"
@@ -163,10 +166,10 @@ export type HomarrDocumentationPath =
| "/docs/getting-started/installation/home-assistant"
| "/docs/getting-started/installation/portainer"
| "/docs/getting-started/installation/qnap"
| "/docs/getting-started/installation/railway"
| "/docs/getting-started/installation/saltbox"
| "/docs/getting-started/installation/source"
| "/docs/getting-started/installation/synology"
| "/docs/getting-started/installation/truenas"
| "/docs/getting-started/installation/unraid"
| "/docs/integrations/containers"
| "/docs/integrations/dns"
@@ -179,6 +182,7 @@ export type HomarrDocumentationPath =
| "/docs/management/api"
| "/docs/management/apps"
| "/docs/management/boards"
| "/docs/management/certificates"
| "/docs/management/integrations"
| "/docs/management/media"
| "/docs/management/search-engines"
+9
View File
@@ -0,0 +1,9 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];
+1
View File
@@ -0,0 +1 @@
export * from "./src";
+38
View File
@@ -0,0 +1,38 @@
{
"name": "@homarr/docker",
"version": "0.1.0",
"private": true,
"license": "MIT",
"type": "module",
"exports": {
".": "./index.ts",
"./env": "./src/env.ts"
},
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@t3-oss/env-nextjs": "^0.11.1",
"dockerode": "^4.0.4"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/dockerode": "^3.3.34",
"eslint": "^9.18.0",
"typescript": "^5.7.3"
}
}
+18
View File
@@ -0,0 +1,18 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
import { shouldSkipEnvValidation } from "@homarr/common/env-validation";
export const env = createEnv({
server: {
// Comma separated list of docker hostnames that can be used to connect to query the docker endpoints (localhost:2375,host.docker.internal:2375, ...)
DOCKER_HOSTNAMES: z.string().optional(),
DOCKER_PORTS: z.string().optional(),
},
runtimeEnv: {
DOCKER_HOSTNAMES: process.env.DOCKER_HOSTNAMES,
DOCKER_PORTS: process.env.DOCKER_PORTS,
},
skipValidation: shouldSkipEnvValidation(),
emptyStringAsUndefined: true,
});
+10
View File
@@ -0,0 +1,10 @@
import type Docker from "dockerode";
export type { DockerInstance } from "./singleton";
export { DockerSingleton } from "./singleton";
export type { ContainerInfo, Container, Port } from "dockerode";
export type { Docker };
export const containerStates = ["created", "running", "paused", "restarting", "exited", "removing", "dead"] as const;
export type ContainerState = (typeof containerStates)[number];
+53
View File
@@ -0,0 +1,53 @@
import Docker from "dockerode";
import { env } from "./env";
export interface DockerInstance {
host: string;
instance: Docker;
}
export class DockerSingleton {
private static instances: DockerInstance[] | null = null;
private createInstances() {
const hostVariable = env.DOCKER_HOSTNAMES;
const portVariable = env.DOCKER_PORTS;
if (hostVariable === undefined || portVariable === undefined) {
return [{ host: "socket", instance: new Docker() }];
}
const hostnames = hostVariable.split(",");
const ports = portVariable.split(",");
if (hostnames.length !== ports.length) {
throw new Error("The number of hosts and ports must match");
}
return hostnames.map((host, i) => {
// Check above ensures that ports[i] is not undefined
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const port = ports[i]!;
return {
host: `${host}:${port}`,
instance: new Docker({
host,
port: parseInt(port, 10),
}),
};
});
}
public static findInstance(host: string): DockerInstance | undefined {
return this.instances?.find((instance) => instance.host === host);
}
public static getInstances(): DockerInstance[] {
if (this.instances) {
return this.instances;
}
this.instances = new DockerSingleton().createInstances();
return this.instances;
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "@homarr/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["*.ts", "src"],
"exclude": ["node_modules"]
}
+1 -1
View File
@@ -26,7 +26,7 @@
"@homarr/common": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/form": "^7.16.0"
"@mantine/form": "^7.16.1"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+1 -1
View File
@@ -37,7 +37,7 @@
"@homarr/validation": "workspace:^0.1.0",
"@jellyfin/sdk": "^0.11.0",
"proxmox-api": "1.1.1",
"undici": "7.2.3",
"undici": "7.3.0",
"xml2js": "^0.6.2"
},
"devDependencies": {
+3 -3
View File
@@ -32,10 +32,10 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^7.16.0",
"@tabler/icons-react": "^3.28.1",
"@mantine/core": "^7.16.1",
"@tabler/icons-react": "^3.29.0",
"dayjs": "^1.11.13",
"next": "15.1.4",
"next": "15.1.6",
"react": "19.0.0",
"react-dom": "19.0.0"
},
+2 -2
View File
@@ -24,8 +24,8 @@
"dependencies": {
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^7.16.0",
"@mantine/hooks": "^7.16.0",
"@mantine/core": "^7.16.1",
"@mantine/hooks": "^7.16.1",
"react": "19.0.0"
},
"devDependencies": {
+2 -2
View File
@@ -24,8 +24,8 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/ui": "workspace:^0.1.0",
"@mantine/notifications": "^7.16.0",
"@tabler/icons-react": "^3.28.1"
"@mantine/notifications": "^7.16.1",
"@tabler/icons-react": "^3.29.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+3 -3
View File
@@ -37,10 +37,10 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^7.16.0",
"@mantine/hooks": "^7.16.0",
"@mantine/core": "^7.16.1",
"@mantine/hooks": "^7.16.1",
"adm-zip": "0.5.16",
"next": "15.1.4",
"next": "15.1.6",
"react": "19.0.0",
"react-dom": "19.0.0",
"superjson": "2.2.2",
@@ -25,7 +25,7 @@ const mapping: Record<OldmarrIntegrationType, IntegrationKind | null> = {
overseerr: "overseerr",
pihole: "piHole",
prowlarr: "prowlarr",
proxmox: null,
proxmox: "proxmox",
qBittorrent: "qBittorrent",
radarr: "radarr",
readarr: "readarr",
+6 -4
View File
@@ -15,7 +15,7 @@ export const mapApp = (
boardSize: BoardSize,
appsMap: Map<string, { id: string }>,
sectionMap: Map<string, { id: string }>,
): InferInsertModel<typeof items> => {
): InferInsertModel<typeof items> | null => {
if (app.area.type === "sidebar") throw new Error("Mapping app in sidebar is not supported");
const shapeForSize = app.shape[boardSize];
@@ -25,7 +25,8 @@ export const mapApp = (
const sectionId = sectionMap.get(app.area.properties.id)?.id;
if (!sectionId) {
throw new Error(`Failed to find section for app appId='${app.id}' sectionId='${app.area.properties.id}'`);
logger.warn(`Failed to find section for app appId='${app.id}' sectionId='${app.area.properties.id}'. Removing app`);
return null;
}
return {
@@ -69,9 +70,10 @@ export const mapWidget = (
const sectionId = sectionMap.get(widget.area.properties.id)?.id;
if (!sectionId) {
throw new Error(
`Failed to find section for widget widgetId='${widget.id}' sectionId='${widget.area.properties.id}'`,
logger.warn(
`Failed to find section for widget widgetId='${widget.id}' sectionId='${widget.area.properties.id}'. Removing widget`,
);
return null;
}
return {
@@ -10,5 +10,5 @@ export const prepareItems = (
) =>
widgets
.map((widget) => mapWidget(widget, boardSize, appsMap, sectionMap))
.filter((widget) => widget !== null)
.concat(apps.map((app) => mapApp(app, boardSize, appsMap, sectionMap)));
.concat(apps.map((app) => mapApp(app, boardSize, appsMap, sectionMap)))
.filter((widget) => widget !== null);
+1
View File
@@ -22,6 +22,7 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@extractus/feed-extractor": "7.1.3",
"@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
+127
View File
@@ -0,0 +1,127 @@
import type { FeedData, FeedEntry } from "@extractus/feed-extractor";
import { extract } from "@extractus/feed-extractor";
import dayjs from "dayjs";
import { z } from "zod";
import type { Modify } from "@homarr/common/types";
import { logger } from "@homarr/log";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
export const rssFeedsRequestHandler = createCachedWidgetRequestHandler({
queryKey: "rssFeedList",
widgetKind: "rssFeed",
async requestAsync(input: { url: string; count: number }) {
const result = (await extract(input.url, {
getExtraEntryFields: (feedEntry) => {
const media = attemptGetImageFromEntry(input.url, feedEntry);
if (!media) {
return {};
}
return {
enclosure: media,
};
},
})) as ExtendedFeedData;
return {
...result,
entries: result.entries?.slice(0, input.count) ?? [],
};
},
cacheDuration: dayjs.duration(5, "minutes"),
});
const attemptGetImageFromEntry = (feedUrl: string, entry: object) => {
const media = getFirstMediaProperty(entry);
if (media !== null) {
return media;
}
return getImageFromStringAsFallback(feedUrl, JSON.stringify(entry));
};
const getImageFromStringAsFallback = (feedUrl: string, content: string) => {
const regex = /https?:\/\/\S+?\.(jpg|jpeg|png|gif|bmp|svg|webp|tiff)/i;
const result = regex.exec(content);
if (result == null) {
return null;
}
console.debug(
`Falling back to regex image search for '${feedUrl}'. Found ${result.length} matches in content: ${content}`,
);
return result[0];
};
const mediaProperties = [
{
path: ["enclosure", "@_url"],
},
{
path: ["media:content", "@_url"],
},
];
/**
* The RSS and Atom standards are poorly adhered to in most of the web.
* We want to show pretty background images on the posts and therefore need to extract
* the enclosure (aka. media images). This function uses the dynamic properties defined above
* to search through the possible paths and detect valid image URLs.
* @param feedObject The object to scan for.
* @returns the value of the first path that is found within the object
*/
const getFirstMediaProperty = (feedObject: object) => {
for (const mediaProperty of mediaProperties) {
let propertyIndex = 0;
let objectAtPath: object = feedObject;
while (propertyIndex < mediaProperty.path.length) {
const key = mediaProperty.path[propertyIndex];
if (key === undefined) {
break;
}
const propertyEntries = Object.entries(objectAtPath);
const propertyEntry = propertyEntries.find(([entryKey]) => entryKey === key);
if (!propertyEntry) {
break;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const [_, propertyEntryValue] = propertyEntry;
objectAtPath = propertyEntryValue as object;
propertyIndex++;
}
const validationResult = z.string().url().safeParse(objectAtPath);
if (!validationResult.success) {
continue;
}
logger.debug(`Found an image in the feed entry: ${validationResult.data}`);
return validationResult.data;
}
return null;
};
/**
* We extend the feed with custom properties.
* This interface adds properties on top of the default ones.
*/
interface ExtendedFeedEntry extends FeedEntry {
enclosure?: string;
}
/**
* We extend the feed with custom properties.
* This interface omits the default entries with our custom definition.
*/
type ExtendedFeedData = Modify<
FeedData,
{
entries?: ExtendedFeedEntry[];
}
>;
export interface RssFeed {
feedUrl: string;
feed: ExtendedFeedData;
}
+6 -6
View File
@@ -32,12 +32,12 @@
"@homarr/modals-collection": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^7.16.0",
"@mantine/hooks": "^7.16.0",
"@mantine/spotlight": "^7.16.0",
"@tabler/icons-react": "^3.28.1",
"jotai": "^2.11.0",
"next": "15.1.4",
"@mantine/core": "^7.16.1",
"@mantine/hooks": "^7.16.1",
"@mantine/spotlight": "^7.16.1",
"@tabler/icons-react": "^3.29.0",
"jotai": "^2.11.1",
"next": "15.1.6",
"react": "19.0.0",
"react-dom": "19.0.0",
"use-deep-compare-effect": "^1.8.1"
+1 -1
View File
@@ -61,7 +61,7 @@ const helpMode = {
{
label: t("submitIssue.label"),
icon: IconBrandGithub,
href: "https://github.com/ajnart/homarr/issues/new/choose",
href: "https://github.com/homarr-labs/homarr/issues/new/choose",
},
{
label: t("discord.label"),
+1 -1
View File
@@ -32,7 +32,7 @@
"dayjs": "^1.11.13",
"deepmerge": "4.3.1",
"mantine-react-table": "2.0.0-beta.8",
"next": "15.1.4",
"next": "15.1.6",
"next-intl": "3.26.3",
"react": "19.0.0",
"react-dom": "19.0.0"
+81 -18
View File
@@ -733,6 +733,14 @@
"password": {
"label": "密码",
"newLabel": "新密码"
},
"tokenId": {
"label": "密鑰 ID",
"newLabel": "新密鑰 ID"
},
"realm": {
"label": "領域",
"newLabel": "新領域"
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": "复制 URL"
},
"open": {
"label": ""
}
}
},
@@ -897,7 +908,7 @@
"passwordRequirements": "密码不符合要求",
"boardAlreadyExists": "具有此名称的面板已存在",
"invalidFileType": "无效的文件类型,例如 {expected}",
"invalidFileName": "",
"invalidFileName": "無效的檔案名稱",
"fileTooLarge": "文件太大,最大大小为 {maxSize}",
"invalidConfiguration": "无效配置",
"groupNameTaken": "用户组名称已存在"
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": "显示文件系统信息"
},
"defaultTab": {
"label": "預設頁面"
},
"sectionIndicatorRequirement": {
"label": "部分指示需求"
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": "获取健康状态失败"
},
"cluster": {
"summary": {
"cpu": "處理器",
"memory": "記憶體"
},
"resource": {
"node": {
"name": "節點"
},
"qemu": {
"name": "虛擬機"
},
"lxc": {
"name": "LXCs"
},
"storage": {
"name": "儲存裝置"
}
},
"popover": {
"rightSection": {
"node": "節點",
"vmId": "虛擬機 ID",
"plugin": "插件"
},
"detail": {
"cpu": "核心",
"memory": "記憶體",
"storage": "儲存裝置",
"uptime": "運行時間",
"haState": "HA 状态",
"storageType": {
"local": "本地存储",
"shared": "共享存储"
}
}
},
"table": {
"header": {
"name": "名称",
"cpu": "处理器",
"memory": "内存",
"node": "节点"
}
}
}
},
"common": {
@@ -2102,7 +2165,7 @@
"docker": "Docker",
"logs": "日志",
"api": "API",
"certificates": "",
"certificates": "证书",
"tasks": "任务"
}
},
@@ -2710,7 +2773,7 @@
"label": "日志"
},
"certificates": {
"label": ""
"label": "证书"
}
},
"settings": {
@@ -3110,39 +3173,39 @@
"certificate": {
"page": {
"list": {
"title": "",
"description": "",
"title": "可信证书",
"description": "Homarr 用于从集成请求数据。",
"noResults": {
"title": ""
"title": "还没有证书"
},
"expires": ""
"expires": "到期时间 {when}"
}
},
"action": {
"create": {
"label": "",
"label": "添加证书",
"notification": {
"success": {
"title": "",
"message": ""
"title": "已添加证书",
"message": "已成功添加证书"
},
"error": {
"title": "",
"message": ""
"title": "添加证书失败",
"message": "无法添加证书"
}
}
},
"remove": {
"label": "",
"confirm": "",
"label": "删除证书",
"confirm": "您确定要删除证书吗?",
"notification": {
"success": {
"title": "",
"message": ""
"title": "证书已删除",
"message": "证书删除成功"
},
"error": {
"title": "",
"message": ""
"title": "证书未删除",
"message": "无法删除证书"
}
}
}
+63
View File
@@ -733,6 +733,14 @@
"password": {
"label": "Heslo",
"newLabel": "Nové heslo"
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": "Zkopírovat URL"
},
"open": {
"label": ""
}
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": "Zobrazit informace o souborovém systému"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": ""
},
"cluster": {
"summary": {
"cpu": "",
"memory": ""
},
"resource": {
"node": {
"name": ""
},
"qemu": {
"name": ""
},
"lxc": {
"name": ""
},
"storage": {
"name": ""
}
},
"popover": {
"rightSection": {
"node": "",
"vmId": "",
"plugin": ""
},
"detail": {
"cpu": "",
"memory": "",
"storage": "",
"uptime": "",
"haState": "",
"storageType": {
"local": "",
"shared": ""
}
}
},
"table": {
"header": {
"name": "",
"cpu": "",
"memory": "",
"node": ""
}
}
}
},
"common": {
File diff suppressed because it is too large. Load diff
+63
View File
@@ -733,6 +733,14 @@
"password": {
"label": "Passwort",
"newLabel": "Neues Passwort"
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": "URL in Zwischenablage kopieren"
},
"open": {
"label": ""
}
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": "Dateisystem Info anzeigen"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": "Fehler beim Abrufen des Gesundheitsstatus"
},
"cluster": {
"summary": {
"cpu": "",
"memory": ""
},
"resource": {
"node": {
"name": ""
},
"qemu": {
"name": ""
},
"lxc": {
"name": ""
},
"storage": {
"name": ""
}
},
"popover": {
"rightSection": {
"node": "",
"vmId": "",
"plugin": ""
},
"detail": {
"cpu": "",
"memory": "",
"storage": "",
"uptime": "",
"haState": "",
"storageType": {
"local": "",
"shared": ""
}
}
},
"table": {
"header": {
"name": "",
"cpu": "",
"memory": "",
"node": ""
}
}
}
},
"common": {
+63
View File
@@ -733,6 +733,14 @@
"password": {
"label": "Κωδικός",
"newLabel": "Νέος κωδικός"
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": ""
},
"open": {
"label": ""
}
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": "Εμφάνιση Πληροφοριών Συστήματος Αρχείων"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": ""
},
"cluster": {
"summary": {
"cpu": "",
"memory": ""
},
"resource": {
"node": {
"name": ""
},
"qemu": {
"name": ""
},
"lxc": {
"name": ""
},
"storage": {
"name": ""
}
},
"popover": {
"rightSection": {
"node": "",
"vmId": "",
"plugin": ""
},
"detail": {
"cpu": "",
"memory": "",
"storage": "",
"uptime": "",
"haState": "",
"storageType": {
"local": "",
"shared": ""
}
}
},
"table": {
"header": {
"name": "",
"cpu": "",
"memory": "",
"node": ""
}
}
}
},
"common": {
+3
View File
@@ -785,6 +785,9 @@
},
"copy": {
"label": "Copy URL"
},
"open": {
"label": "Open media"
}
}
},
+63
View File
@@ -733,6 +733,14 @@
"password": {
"label": "Contraseña",
"newLabel": "Nueva contraseña"
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": ""
},
"open": {
"label": ""
}
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": "Mostrar información del sistema de archivos"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": ""
},
"cluster": {
"summary": {
"cpu": "",
"memory": ""
},
"resource": {
"node": {
"name": ""
},
"qemu": {
"name": ""
},
"lxc": {
"name": ""
},
"storage": {
"name": ""
}
},
"popover": {
"rightSection": {
"node": "",
"vmId": "",
"plugin": ""
},
"detail": {
"cpu": "",
"memory": "",
"storage": "",
"uptime": "",
"haState": "",
"storageType": {
"local": "",
"shared": ""
}
}
},
"table": {
"header": {
"name": "",
"cpu": "",
"memory": "",
"node": ""
}
}
}
},
"common": {
+63
View File
@@ -733,6 +733,14 @@
"password": {
"label": "",
"newLabel": ""
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": ""
},
"open": {
"label": ""
}
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": ""
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": ""
},
"cluster": {
"summary": {
"cpu": "",
"memory": ""
},
"resource": {
"node": {
"name": ""
},
"qemu": {
"name": ""
},
"lxc": {
"name": ""
},
"storage": {
"name": ""
}
},
"popover": {
"rightSection": {
"node": "",
"vmId": "",
"plugin": ""
},
"detail": {
"cpu": "",
"memory": "",
"storage": "",
"uptime": "",
"haState": "",
"storageType": {
"local": "",
"shared": ""
}
}
},
"table": {
"header": {
"name": "",
"cpu": "",
"memory": "",
"node": ""
}
}
}
},
"common": {
+63
View File
@@ -733,6 +733,14 @@
"password": {
"label": "Mot de passe",
"newLabel": "Nouveau mot de passe"
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": "Copier l'URL"
},
"open": {
"label": ""
}
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": "Afficher les infos sur le système de fichiers"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": ""
},
"cluster": {
"summary": {
"cpu": "",
"memory": ""
},
"resource": {
"node": {
"name": ""
},
"qemu": {
"name": ""
},
"lxc": {
"name": ""
},
"storage": {
"name": ""
}
},
"popover": {
"rightSection": {
"node": "",
"vmId": "",
"plugin": ""
},
"detail": {
"cpu": "",
"memory": "",
"storage": "",
"uptime": "",
"haState": "",
"storageType": {
"local": "",
"shared": ""
}
}
},
"table": {
"header": {
"name": "",
"cpu": "",
"memory": "",
"node": ""
}
}
}
},
"common": {
+63
View File
@@ -733,6 +733,14 @@
"password": {
"label": "סיסמה",
"newLabel": "סיסמה חדשה"
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": "העתק קישור"
},
"open": {
"label": ""
}
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": "הצג מידע על מערכת הקבצים"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": "נכשל בעדכון סטטוס בריאות המערכת"
},
"cluster": {
"summary": {
"cpu": "",
"memory": ""
},
"resource": {
"node": {
"name": ""
},
"qemu": {
"name": ""
},
"lxc": {
"name": ""
},
"storage": {
"name": ""
}
},
"popover": {
"rightSection": {
"node": "",
"vmId": "",
"plugin": ""
},
"detail": {
"cpu": "",
"memory": "",
"storage": "",
"uptime": "",
"haState": "",
"storageType": {
"local": "",
"shared": ""
}
}
},
"table": {
"header": {
"name": "",
"cpu": "",
"memory": "",
"node": ""
}
}
}
},
"common": {
+63
View File
@@ -733,6 +733,14 @@
"password": {
"label": "Lozinka",
"newLabel": ""
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": ""
},
"open": {
"label": ""
}
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": ""
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": ""
},
"cluster": {
"summary": {
"cpu": "",
"memory": ""
},
"resource": {
"node": {
"name": ""
},
"qemu": {
"name": ""
},
"lxc": {
"name": ""
},
"storage": {
"name": ""
}
},
"popover": {
"rightSection": {
"node": "",
"vmId": "",
"plugin": ""
},
"detail": {
"cpu": "",
"memory": "",
"storage": "",
"uptime": "",
"haState": "",
"storageType": {
"local": "",
"shared": ""
}
}
},
"table": {
"header": {
"name": "",
"cpu": "",
"memory": "",
"node": ""
}
}
}
},
"common": {
+63
View File
@@ -733,6 +733,14 @@
"password": {
"label": "Jelszó",
"newLabel": "Új jelszó"
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": "URL másolása"
},
"open": {
"label": ""
}
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": "Fájlrendszer-információk megjelenítése"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": ""
},
"cluster": {
"summary": {
"cpu": "",
"memory": ""
},
"resource": {
"node": {
"name": ""
},
"qemu": {
"name": ""
},
"lxc": {
"name": ""
},
"storage": {
"name": ""
}
},
"popover": {
"rightSection": {
"node": "",
"vmId": "",
"plugin": ""
},
"detail": {
"cpu": "",
"memory": "",
"storage": "",
"uptime": "",
"haState": "",
"storageType": {
"local": "",
"shared": ""
}
}
},
"table": {
"header": {
"name": "",
"cpu": "",
"memory": "",
"node": ""
}
}
}
},
"common": {
+63
View File
@@ -733,6 +733,14 @@
"password": {
"label": "",
"newLabel": "Nuova password"
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": ""
},
"open": {
"label": ""
}
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": "Mostra Informazioni Filesystem"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": ""
},
"cluster": {
"summary": {
"cpu": "",
"memory": ""
},
"resource": {
"node": {
"name": ""
},
"qemu": {
"name": ""
},
"lxc": {
"name": ""
},
"storage": {
"name": ""
}
},
"popover": {
"rightSection": {
"node": "",
"vmId": "",
"plugin": ""
},
"detail": {
"cpu": "",
"memory": "",
"storage": "",
"uptime": "",
"haState": "",
"storageType": {
"local": "",
"shared": ""
}
}
},
"table": {
"header": {
"name": "",
"cpu": "",
"memory": "",
"node": ""
}
}
}
},
"common": {
+63
View File
@@ -733,6 +733,14 @@
"password": {
"label": "パスワード",
"newLabel": "新しいパスワード"
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": ""
},
"open": {
"label": ""
}
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": "ファイルシステム情報を表示"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": ""
},
"cluster": {
"summary": {
"cpu": "",
"memory": ""
},
"resource": {
"node": {
"name": ""
},
"qemu": {
"name": ""
},
"lxc": {
"name": ""
},
"storage": {
"name": ""
}
},
"popover": {
"rightSection": {
"node": "",
"vmId": "",
"plugin": ""
},
"detail": {
"cpu": "",
"memory": "",
"storage": "",
"uptime": "",
"haState": "",
"storageType": {
"local": "",
"shared": ""
}
}
},
"table": {
"header": {
"name": "",
"cpu": "",
"memory": "",
"node": ""
}
}
}
},
"common": {
+63
View File
@@ -733,6 +733,14 @@
"password": {
"label": "비밀번호",
"newLabel": ""
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": ""
},
"open": {
"label": ""
}
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": ""
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": ""
},
"cluster": {
"summary": {
"cpu": "",
"memory": ""
},
"resource": {
"node": {
"name": ""
},
"qemu": {
"name": ""
},
"lxc": {
"name": ""
},
"storage": {
"name": ""
}
},
"popover": {
"rightSection": {
"node": "",
"vmId": "",
"plugin": ""
},
"detail": {
"cpu": "",
"memory": "",
"storage": "",
"uptime": "",
"haState": "",
"storageType": {
"local": "",
"shared": ""
}
}
},
"table": {
"header": {
"name": "",
"cpu": "",
"memory": "",
"node": ""
}
}
}
},
"common": {
+63
View File
@@ -733,6 +733,14 @@
"password": {
"label": "Slaptažodis",
"newLabel": "Naujas slaptažodis"
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": ""
},
"open": {
"label": ""
}
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": ""
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": ""
},
"cluster": {
"summary": {
"cpu": "",
"memory": ""
},
"resource": {
"node": {
"name": ""
},
"qemu": {
"name": ""
},
"lxc": {
"name": ""
},
"storage": {
"name": ""
}
},
"popover": {
"rightSection": {
"node": "",
"vmId": "",
"plugin": ""
},
"detail": {
"cpu": "",
"memory": "",
"storage": "",
"uptime": "",
"haState": "",
"storageType": {
"local": "",
"shared": ""
}
}
},
"table": {
"header": {
"name": "",
"cpu": "",
"memory": "",
"node": ""
}
}
}
},
"common": {
+63
View File
@@ -733,6 +733,14 @@
"password": {
"label": "Parole",
"newLabel": "Jauna parole"
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": ""
},
"open": {
"label": ""
}
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": "Rādīt failu sistēmas informāciju"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": ""
},
"cluster": {
"summary": {
"cpu": "",
"memory": ""
},
"resource": {
"node": {
"name": ""
},
"qemu": {
"name": ""
},
"lxc": {
"name": ""
},
"storage": {
"name": ""
}
},
"popover": {
"rightSection": {
"node": "",
"vmId": "",
"plugin": ""
},
"detail": {
"cpu": "",
"memory": "",
"storage": "",
"uptime": "",
"haState": "",
"storageType": {
"local": "",
"shared": ""
}
}
},
"table": {
"header": {
"name": "",
"cpu": "",
"memory": "",
"node": ""
}
}
}
},
"common": {
+138 -75
View File
@@ -213,10 +213,10 @@
"changeDefaultSearchEngine": {
"notification": {
"success": {
"message": ""
"message": "Standaard zoekmachine succesvol gewijzigd"
},
"error": {
"message": ""
"message": "Standaard zoekmachine kan niet worden gewijzigd"
}
}
},
@@ -499,7 +499,7 @@
}
},
"app": {
"search": "",
"search": "Vind een app",
"page": {
"list": {
"title": "Apps",
@@ -625,8 +625,8 @@
"label": "URL"
},
"attemptSearchEngineCreation": {
"label": "",
"description": ""
"label": "Zoekmachine aanmaken",
"description": "Integratie \"{kind}\" kan worden gebruikt met de zoekmachines. Controleer dit om automatisch de zoekmachine te configureren."
}
},
"action": {
@@ -733,6 +733,14 @@
"password": {
"label": "Wachtwoord",
"newLabel": "Nieuw wachtwoord"
},
"tokenId": {
"label": "Token ID",
"newLabel": "Nieuwe token ID"
},
"realm": {
"label": "Realm",
"newLabel": "Nieuwe Realm"
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": "URL kopiëren"
},
"open": {
"label": "Media openen"
}
}
},
@@ -897,7 +908,7 @@
"passwordRequirements": "Wachtwoord voldoet niet aan de eisen",
"boardAlreadyExists": "Er bestaat al een bord met deze naam",
"invalidFileType": "Ongeldig bestandstype, verwachtte {expected}",
"invalidFileName": "",
"invalidFileName": "Ongeldige bestandsnaam",
"fileTooLarge": "Bestand is te groot, maximale grootte is {maxSize}",
"invalidConfiguration": "Ongeldige configuratie",
"groupNameTaken": "Groepsnaam al in gebruik"
@@ -967,7 +978,7 @@
},
"create": {
"title": "Kies een item om toe te voegen",
"search": "",
"search": "Items filteren",
"addToBoard": "Toevoegen aan bord"
},
"moveResize": {
@@ -1094,7 +1105,7 @@
"dnsQueriesToday": "Aanvragen vandaag",
"domainsBeingBlocked": "Domeinen op blokkadelijst"
},
"domainsTooltip": ""
"domainsTooltip": "Vanwege meerdere integraties kan Homarr het exacte aantal geblokkeerde domeinen niet berekenen"
},
"dnsHoleControls": {
"name": "DNS-hole bedieningen",
@@ -1170,22 +1181,22 @@
}
},
"minecraftServerStatus": {
"name": "",
"description": "",
"name": "Minecraft server status",
"description": "Geeft de status van een Minecraft server weer",
"option": {
"title": {
"label": ""
"label": "Titel"
},
"domain": {
"label": ""
"label": "Serveradres"
},
"isBedrockServer": {
"label": ""
"label": "Bedrock server"
}
},
"status": {
"online": "",
"offline": ""
"online": "Online",
"offline": "Offline"
}
},
"notebook": {
@@ -1283,7 +1294,7 @@
},
"error": {
"noUrl": "Geen iFrame URL opgegeven",
"unsupportedProtocol": "",
"unsupportedProtocol": "De opgegeven URL gebruikt een niet-ondersteund protocol. Gebruik een van ({supportedProtocols})",
"noBrowerSupport": "Je browser ondersteunt geen iframes. Update je browser."
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": "Bestandssysteem info weergeven"
},
"defaultTab": {
"label": "Standaard tab"
},
"sectionIndicatorRequirement": {
"label": "Sectie indicator vereiste"
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": "Gezondheidsstatus ophalen mislukt"
},
"cluster": {
"summary": {
"cpu": "CPU",
"memory": "RAM"
},
"resource": {
"node": {
"name": "Nodes"
},
"qemu": {
"name": "VM's"
},
"lxc": {
"name": "LXC's"
},
"storage": {
"name": "Opslagruimte"
}
},
"popover": {
"rightSection": {
"node": "Node",
"vmId": "VM ID",
"plugin": "Plugin"
},
"detail": {
"cpu": "Cores",
"memory": "Geheugen",
"storage": "Opslagruimte",
"uptime": "Uptime",
"haState": "HA status",
"storageType": {
"local": "Lokale opslag",
"shared": "Gedeelde opslag"
}
}
},
"table": {
"header": {
"name": "Naam",
"cpu": "CPU",
"memory": "RAM",
"node": "Node"
}
}
}
},
"common": {
@@ -1497,9 +1560,9 @@
"description": "De huidige streams op je mediaservers weergeven",
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
"user": "Gebruiker",
"name": "Naam",
"id": "Id"
}
},
"downloads": {
@@ -1776,16 +1839,16 @@
"board": {
"action": {
"duplicate": {
"title": "",
"message": "",
"title": "Bord dupliceren",
"message": "Dit zal het bord {name} dupliceren met al zijn inhoud. Als widgets refereren aan integraties, die je niet mag gebruiken, zullen ze worden verwijderd.",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Bord gedupliceerd",
"message": "Het bord is succesvol gedupliceerd"
},
"error": {
"title": "",
"message": ""
"title": "Kan board niet dupliceren",
"message": "Het bord kon niet worden gedupliceerd"
}
}
},
@@ -2102,7 +2165,7 @@
"docker": "Docker",
"logs": "Logs",
"api": "API",
"certificates": "",
"certificates": "Certificaten",
"tasks": "Taken"
}
},
@@ -2156,14 +2219,14 @@
}
},
"setMobileHomeBoard": {
"label": "",
"label": "Instellen als je mobiele bord",
"badge": {
"label": "",
"tooltip": ""
"label": "Mobiel",
"tooltip": "Dit bord zal getoond worden als je mobiele bord"
}
},
"duplicate": {
"label": ""
"label": "Bord dupliceren"
},
"delete": {
"label": "Permanent verwijderen",
@@ -2199,13 +2262,13 @@
"item": {
"language": "Taal & regio",
"board": {
"title": "",
"title": "Home bord",
"type": {
"general": "",
"mobile": ""
"general": "Algemeen",
"mobile": "Mobiel"
}
},
"defaultSearchEngine": "",
"defaultSearchEngine": "Standaard zoekmachine",
"firstDayOfWeek": "Eerste dag van de week",
"accessibility": "Toegankelijkheid"
}
@@ -2364,15 +2427,15 @@
"title": "Borden",
"homeBoard": {
"label": "Globaal home-bord",
"mobileLabel": "",
"mobileLabel": "Globaal mobiel bord",
"description": "Alleen openbare borden zijn beschikbaar voor selectie"
}
},
"search": {
"title": "",
"title": "Zoeken",
"defaultSearchEngine": {
"label": "",
"description": ""
"label": "Globale standaard zoekmachine",
"description": "Integratie zoekmachines kunnen hier niet geselecteerd worden"
}
},
"appearance": {
@@ -2403,7 +2466,7 @@
},
"job": {
"minecraftServerStatus": {
"label": ""
"label": "Minecraft server status"
},
"iconsUpdater": {
"label": "Icoon updater"
@@ -2601,19 +2664,19 @@
}
},
"addToHomarr": {
"label": "",
"label": "Toevoegen aan Homarr",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Toegevoegd aan Homarr",
"message": "Geselecteerde apps zijn toegevoegd aan Homarr"
},
"error": {
"title": "",
"message": ""
"title": "Kon niet toevoegen aan Homarr",
"message": "Geselecteerde apps konden niet worden toegevoegd aan Homarr"
}
},
"modal": {
"title": ""
"title": "Docker container('s) toevoegen aan Homarr"
}
}
},
@@ -2710,7 +2773,7 @@
"label": "Logs"
},
"certificates": {
"label": ""
"label": "Certificaten"
}
},
"settings": {
@@ -2758,7 +2821,7 @@
"label": "Instellen als home-bord"
},
"mobileBoard": {
"label": ""
"label": "Instellen als mobiel bord"
},
"settings": {
"label": "Instellingen openen"
@@ -2826,9 +2889,9 @@
}
},
"media": {
"requestMovie": "",
"requestSeries": "",
"openIn": ""
"requestMovie": "Film aanvragen",
"requestSeries": "Series aanvragen",
"openIn": "Openen in {kind}"
},
"external": {
"help": "Gebruik een externe zoekmachine",
@@ -2897,20 +2960,20 @@
"home": {
"group": {
"search": {
"title": "",
"title": "Zoeken",
"option": {
"other": {
"label": ""
"label": "Zoeken met andere zoekmachine"
},
"no-default": {
"label": "",
"description": ""
"label": "Geen standaard zoekmachine",
"description": "Stel een standaard zoekmachine in in voorkeuren"
},
"search": {
"label": ""
"label": "Zoeken naar \"{query}\" met {name}"
},
"from-integration": {
"description": ""
"description": "Typ om te zoeken"
}
}
},
@@ -3092,15 +3155,15 @@
"media": {
"request": {
"modal": {
"title": "",
"title": "\"{name}\" aanvragen",
"table": {
"header": {
"season": "",
"episodes": ""
"season": "Seizoen",
"episodes": "Afleveringen"
}
},
"button": {
"send": ""
"send": "Aanvraag versturen"
}
}
}
@@ -3110,39 +3173,39 @@
"certificate": {
"page": {
"list": {
"title": "",
"description": "",
"title": "Vertrouwde certificaten",
"description": "Gebruikt door Homarr om gegevens op te vragen van integraties.",
"noResults": {
"title": ""
"title": "Er zijn nog geen certificaten"
},
"expires": ""
"expires": "Verloopt {when}"
}
},
"action": {
"create": {
"label": "",
"label": "Certificaat toevoegen",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Certificaat toegevoegd",
"message": "Het certificaat is succesvol toegevoegd"
},
"error": {
"title": "",
"message": ""
"title": "Certificaat toevoegen mislukt",
"message": "Het certificaat kon niet worden toegevoegd"
}
}
},
"remove": {
"label": "",
"confirm": "",
"label": "Certificaten verwijderen",
"confirm": "Weet je zeker dat je het certificaat wilt verwijderen?",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Certificaat verwijderd",
"message": "Het certificaat is succesvol verwijderd"
},
"error": {
"title": "",
"message": ""
"title": "Certificaat niet verwijderd",
"message": "Certificaat kon niet worden verwijderd"
}
}
}
+63
View File
@@ -733,6 +733,14 @@
"password": {
"label": "Passord",
"newLabel": "Nytt passord"
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": ""
},
"open": {
"label": ""
}
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": "Vis filsysteminfo"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": ""
},
"cluster": {
"summary": {
"cpu": "",
"memory": ""
},
"resource": {
"node": {
"name": ""
},
"qemu": {
"name": ""
},
"lxc": {
"name": ""
},
"storage": {
"name": ""
}
},
"popover": {
"rightSection": {
"node": "",
"vmId": "",
"plugin": ""
},
"detail": {
"cpu": "",
"memory": "",
"storage": "",
"uptime": "",
"haState": "",
"storageType": {
"local": "",
"shared": ""
}
}
},
"table": {
"header": {
"name": "",
"cpu": "",
"memory": "",
"node": ""
}
}
}
},
"common": {
+63
View File
@@ -733,6 +733,14 @@
"password": {
"label": "Hasło",
"newLabel": "Nowe hasło"
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": "Kopiuj adres URL"
},
"open": {
"label": ""
}
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": "Pokaż informacje o systemie plików"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": "Nie udało się pobrać stanu zdrowia"
},
"cluster": {
"summary": {
"cpu": "",
"memory": ""
},
"resource": {
"node": {
"name": ""
},
"qemu": {
"name": ""
},
"lxc": {
"name": ""
},
"storage": {
"name": ""
}
},
"popover": {
"rightSection": {
"node": "",
"vmId": "",
"plugin": ""
},
"detail": {
"cpu": "",
"memory": "",
"storage": "",
"uptime": "",
"haState": "",
"storageType": {
"local": "",
"shared": ""
}
}
},
"table": {
"header": {
"name": "",
"cpu": "",
"memory": "",
"node": ""
}
}
}
},
"common": {
+113 -50
View File
@@ -2,123 +2,123 @@
"init": {
"step": {
"start": {
"title": "",
"subtitle": "",
"description": "",
"title": "Bem-vindo ao Homarr",
"subtitle": "Vamos começar configurando a sua instância Homarr.",
"description": "Para começar, por favor selecione como você deseja configurar sua instância Homarr.",
"action": {
"scratch": "",
"importOldmarr": ""
"scratch": "Começar do zero",
"importOldmarr": "Importe de uma instância Homarr anterior a 1,0"
}
},
"import": {
"title": "",
"subtitle": "",
"title": "Importar dados",
"subtitle": "Você pode importar dados de uma instância existente do Homarr.",
"dropzone": {
"title": "",
"description": ""
"title": "Arraste o arquivo zip aqui ou clique para procurar",
"description": "O arquivo zip enviado será processado e você poderá selecionar o que deseja importar"
},
"fileInfo": {
"action": {
"change": ""
"change": "Alterar arquivo"
}
},
"importSettings": {
"title": "",
"description": ""
"title": "Importar configurações",
"description": "Configure o comportamento da importação"
},
"boardSelection": {
"title": "",
"description": "",
"title": "{count} quadros encontrados",
"description": "Escolha todos os quadros com o tamanho que você deseja importar",
"action": {
"selectAll": "",
"unselectAll": ""
"selectAll": "Selecionar todos",
"unselectAll": "Desmarcar Todos"
}
},
"summary": {
"title": "",
"description": "",
"title": "Resumo da importação",
"description": "No resumo abaixo, você pode ver o que será importado",
"action": {
"import": ""
"import": "Confirmar a importação e continuar"
},
"entities": {
"apps": "Aplicativos",
"boards": "Placas",
"integrations": "",
"credentialUsers": ""
"integrations": "Integrações",
"credentialUsers": "Usuários credenciados"
}
},
"tokenModal": {
"title": "",
"title": "Insira o token de importação",
"field": {
"token": {
"label": "",
"description": ""
"label": "Token",
"description": "Insira o token de importação mostrado em sua instância homarr anterior"
}
},
"notification": {
"error": {
"title": "",
"message": ""
"title": "Token inválido",
"message": "O token que você inseriu é inválido"
}
}
}
},
"user": {
"title": "",
"subtitle": "",
"title": "Usuário Administrador",
"subtitle": "Especifique as credenciais para seu usuário administrador.",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Usuário criado",
"message": "O usuário foi criado com sucesso"
},
"error": {
"title": ""
"title": "Falha ao criar usuário"
}
}
},
"group": {
"title": "",
"subtitle": "",
"title": "Grupo externo",
"subtitle": "Especifique o grupo que deve ser usado para usuários externos.",
"form": {
"name": {
"label": "",
"description": ""
"label": "Nome do grupo",
"description": "O nome tem que coincidir com o grupo administrador do provedor externo"
}
}
},
"settings": {
"title": "Configurações",
"subtitle": ""
"subtitle": "Definir as configurações do servidor."
},
"finish": {
"title": "",
"subtitle": "",
"description": "",
"title": "Concluir configuração",
"subtitle": "Você está pronto para começar!",
"description": "Você concluiu com sucesso o processo de configuração. Agora você pode começar a usar o Homarr. Selecione sua próxima ação:",
"action": {
"goToBoard": "",
"createBoard": "",
"inviteUser": "",
"docs": ""
"goToBoard": "Ir para o quadro {name}",
"createBoard": "Crie seu primeiro quadro",
"inviteUser": "Convidar outros usuários",
"docs": "Ler a documentação"
}
}
},
"backToStart": ""
"backToStart": "Voltar ao início"
},
"user": {
"title": "Usuários",
"name": "Usuário",
"page": {
"login": {
"title": "",
"subtitle": ""
"title": "Acesse a sua conta",
"subtitle": "Bem-vindo(a) de volta! Por favor, insira suas credenciais"
},
"invite": {
"title": "",
"subtitle": "",
"description": ""
"title": "Junte-se ao Homarr",
"subtitle": "Bem-vindo ao Homarr! Por favor, crie sua conta",
"description": "Você foi convidado(a) por {username}"
},
"init": {
"title": "",
"title": "Nova instalação do Homarr",
"subtitle": ""
}
},
@@ -733,6 +733,14 @@
"password": {
"label": "Senha",
"newLabel": "Nova senha"
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": ""
},
"open": {
"label": ""
}
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": "Mostrar informações do sistema de arquivos"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": ""
},
"cluster": {
"summary": {
"cpu": "",
"memory": ""
},
"resource": {
"node": {
"name": ""
},
"qemu": {
"name": ""
},
"lxc": {
"name": ""
},
"storage": {
"name": ""
}
},
"popover": {
"rightSection": {
"node": "",
"vmId": "",
"plugin": ""
},
"detail": {
"cpu": "",
"memory": "",
"storage": "",
"uptime": "",
"haState": "",
"storageType": {
"local": "",
"shared": ""
}
}
},
"table": {
"header": {
"name": "",
"cpu": "",
"memory": "",
"node": ""
}
}
}
},
"common": {
+63
View File
@@ -733,6 +733,14 @@
"password": {
"label": "Parola",
"newLabel": "Parolă nouă"
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": ""
},
"open": {
"label": ""
}
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": "Afișare informații despre sistemul de fișiere"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": ""
},
"cluster": {
"summary": {
"cpu": "",
"memory": ""
},
"resource": {
"node": {
"name": ""
},
"qemu": {
"name": ""
},
"lxc": {
"name": ""
},
"storage": {
"name": ""
}
},
"popover": {
"rightSection": {
"node": "",
"vmId": "",
"plugin": ""
},
"detail": {
"cpu": "",
"memory": "",
"storage": "",
"uptime": "",
"haState": "",
"storageType": {
"local": "",
"shared": ""
}
}
},
"table": {
"header": {
"name": "",
"cpu": "",
"memory": "",
"node": ""
}
}
}
},
"common": {
+139 -76
View File
@@ -213,10 +213,10 @@
"changeDefaultSearchEngine": {
"notification": {
"success": {
"message": ""
"message": "Поисковая система по умолчанию успешно изменена"
},
"error": {
"message": ""
"message": "Не удалось изменить поисковую систему по умолчанию"
}
}
},
@@ -499,7 +499,7 @@
}
},
"app": {
"search": "",
"search": "Поиск приложений",
"page": {
"list": {
"title": "Приложения",
@@ -625,8 +625,8 @@
"label": "URL-адрес"
},
"attemptSearchEngineCreation": {
"label": "",
"description": ""
"label": "Создать поисковую систему",
"description": "Интеграцию \"{kind}\" можно использовать с поисковыми системами. Включите эту опцию для автоматической настройки поисковой системы."
}
},
"action": {
@@ -733,6 +733,14 @@
"password": {
"label": "Пароль",
"newLabel": "Новый пароль"
},
"tokenId": {
"label": "ID токена",
"newLabel": "Новый ID токена"
},
"realm": {
"label": "Область",
"newLabel": "Новая область"
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": "Копировать URL"
},
"open": {
"label": ""
}
}
},
@@ -897,7 +908,7 @@
"passwordRequirements": "Пароль не соответствует требованиям",
"boardAlreadyExists": "Панель с таким именем уже существует",
"invalidFileType": "Неверный тип файла, ожидается {expected}",
"invalidFileName": "",
"invalidFileName": "Недопустимое имя файла",
"fileTooLarge": "Файл слишком большой, максимальный размер {maxSize}",
"invalidConfiguration": "Неверная конфигурация",
"groupNameTaken": "Имя группы уже занято"
@@ -967,7 +978,7 @@
},
"create": {
"title": "Выберите тип элемента",
"search": "",
"search": "Фильтр элементов",
"addToBoard": "Добавить на панель"
},
"moveResize": {
@@ -1094,7 +1105,7 @@
"dnsQueriesToday": "DNS-запросов сегодня",
"domainsBeingBlocked": "Доменов в списке блокировки"
},
"domainsTooltip": ""
"domainsTooltip": "Из-за наличия нескольких интеграций Homarr не может рассчитать точное количество заблокированных доменов"
},
"dnsHoleControls": {
"name": "Управление DNS-фильтром",
@@ -1170,22 +1181,22 @@
}
},
"minecraftServerStatus": {
"name": "",
"description": "",
"name": "Статус сервера Minecraft",
"description": "Отображает текущий статус и информацию о сервере Minecraft",
"option": {
"title": {
"label": ""
"label": "Заголовок"
},
"domain": {
"label": ""
"label": "Домен сервера"
},
"isBedrockServer": {
"label": ""
"label": "Сервер Bedrock Edition"
}
},
"status": {
"online": "",
"offline": ""
"online": "В сети",
"offline": "Не в сети"
}
},
"notebook": {
@@ -1283,7 +1294,7 @@
},
"error": {
"noUrl": "URL для iFrame не указан",
"unsupportedProtocol": "",
"unsupportedProtocol": "Указанный URL использует неподдерживаемый протокол. Пожалуйста, используйте один из ({supportedProtocols})",
"noBrowerSupport": "Ваш браузер не поддерживает iframes. Пожалуйста, обновите браузер."
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": "Показать информацию о файловой системе"
},
"defaultTab": {
"label": "Вкладка по умолчанию"
},
"sectionIndicatorRequirement": {
"label": "Пороговое значение индикатора раздела"
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": "Не удалось получить данные о состоянии системы"
},
"cluster": {
"summary": {
"cpu": "ЦП",
"memory": "Память"
},
"resource": {
"node": {
"name": "Узлы"
},
"qemu": {
"name": "Виртуальные машины"
},
"lxc": {
"name": "Контейнеры LXC"
},
"storage": {
"name": "Хранилище"
}
},
"popover": {
"rightSection": {
"node": "Узел",
"vmId": "ID виртуальной машины",
"plugin": "Плагин"
},
"detail": {
"cpu": "Ядер",
"memory": "Память",
"storage": "Хранилище",
"uptime": "Время работы",
"haState": "Состояние HA",
"storageType": {
"local": "Локальное",
"shared": "Общее"
}
}
},
"table": {
"header": {
"name": "Название",
"cpu": "ЦП",
"memory": "Память",
"node": "Узел"
}
}
}
},
"common": {
@@ -1497,9 +1560,9 @@
"description": "Отображает текущие сеансы воспроизведения на медиасерверах",
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
"user": "Пользователь",
"name": "Название",
"id": "Id"
}
},
"downloads": {
@@ -1776,16 +1839,16 @@
"board": {
"action": {
"duplicate": {
"title": "",
"message": "",
"title": "Дублировать панель",
"message": "Это действие создаст копию панели {name} со всем её содержимым. Если виджеты используют интеграции, к которым у вас нет доступа, они будут удалены.",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Панель продублирована",
"message": "Панель успешно продублирована"
},
"error": {
"title": "",
"message": ""
"title": "Ошибка дублирования",
"message": "Не удалось продублировать панель"
}
}
},
@@ -2102,7 +2165,7 @@
"docker": "Docker",
"logs": "Логи",
"api": "API",
"certificates": "",
"certificates": "Сертификаты",
"tasks": "Задачи"
}
},
@@ -2156,14 +2219,14 @@
}
},
"setMobileHomeBoard": {
"label": "",
"label": "Сделать домашней панелью для мобильных устройств",
"badge": {
"label": "",
"tooltip": ""
"label": "Мобильная",
"tooltip": "Эта панель будет показываться как ваша домашняя панель на мобильных устройствах"
}
},
"duplicate": {
"label": ""
"label": "Дублировать"
},
"delete": {
"label": "Удалить навсегда",
@@ -2199,13 +2262,13 @@
"item": {
"language": "Язык и регион",
"board": {
"title": "",
"title": "Домашняя панель",
"type": {
"general": "",
"mobile": ""
"general": "Основная",
"mobile": "Мобильная"
}
},
"defaultSearchEngine": "",
"defaultSearchEngine": "Поисковая система по умолчанию",
"firstDayOfWeek": "Первый день недели",
"accessibility": "Специальные возможности"
}
@@ -2363,16 +2426,16 @@
"board": {
"title": "Панели",
"homeBoard": {
"label": "Общая домашняя панель",
"mobileLabel": "",
"label": "Глобальная домашняя панель",
"mobileLabel": "Глобальная мобильная панель",
"description": "Для выбора доступны только публичные панели"
}
},
"search": {
"title": "",
"title": "Поиск",
"defaultSearchEngine": {
"label": "",
"description": ""
"label": "Глобальная поисковая система",
"description": "Поисковые системы из интеграций нельзя выбрать здесь"
}
},
"appearance": {
@@ -2403,7 +2466,7 @@
},
"job": {
"minecraftServerStatus": {
"label": ""
"label": "Статус сервера Minecraft"
},
"iconsUpdater": {
"label": "Обновление иконок"
@@ -2601,19 +2664,19 @@
}
},
"addToHomarr": {
"label": "",
"label": "Добавить в Homarr",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Приложение добавлено",
"message": "Приложение успешно добавлено в Homarr"
},
"error": {
"title": "",
"message": ""
"title": "Не удалось добавить",
"message": "Не удалось добавить приложение в Homarr"
}
},
"modal": {
"title": ""
"title": "Добавить контейнер(ы) Docker в Homarr"
}
}
},
@@ -2710,7 +2773,7 @@
"label": "Логи"
},
"certificates": {
"label": ""
"label": "Сертификаты"
}
},
"settings": {
@@ -2758,7 +2821,7 @@
"label": "Сделать домашней панелью"
},
"mobileBoard": {
"label": ""
"label": "Сделать домашней панелью для мобильных устройств"
},
"settings": {
"label": "Открыть настройки"
@@ -2826,9 +2889,9 @@
}
},
"media": {
"requestMovie": "",
"requestSeries": "",
"openIn": ""
"requestMovie": "Запросить фильм",
"requestSeries": "Запросить сериал",
"openIn": "Открыть в {kind}"
},
"external": {
"help": "Использовать внешнюю поисковую систему",
@@ -2897,20 +2960,20 @@
"home": {
"group": {
"search": {
"title": "",
"title": "Поиск",
"option": {
"other": {
"label": ""
"label": "Искать в другой поисковой системе"
},
"no-default": {
"label": "",
"description": ""
"label": "Нет поисковой системы по умолчанию",
"description": "Установите поисковую систему по умолчанию в настройках"
},
"search": {
"label": ""
"label": "Искать \"{query}\" через {name}"
},
"from-integration": {
"description": ""
"description": "Начните вводить для поиска"
}
}
},
@@ -3092,15 +3155,15 @@
"media": {
"request": {
"modal": {
"title": "",
"title": "Запрос \"{name}\"",
"table": {
"header": {
"season": "",
"episodes": ""
"season": "Сезон",
"episodes": "Серии"
}
},
"button": {
"send": ""
"send": "Отправить запрос"
}
}
}
@@ -3110,39 +3173,39 @@
"certificate": {
"page": {
"list": {
"title": "",
"description": "",
"title": "Доверенные сертификаты",
"description": "Используются Homarr для запроса данных из интеграций.",
"noResults": {
"title": ""
"title": "Доверенные сертификаты отсутствуют"
},
"expires": ""
"expires": "Истекает {when}"
}
},
"action": {
"create": {
"label": "",
"label": "Создать сертификат",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Сертификат создан",
"message": "Сертификат успешно создан"
},
"error": {
"title": "",
"message": ""
"title": "Не удалось создать сертификат",
"message": "Произошла ошибка при создании сертификата"
}
}
},
"remove": {
"label": "",
"confirm": "",
"label": "Удалить сертификат",
"confirm": "Вы уверены, что хотите удалить этот сертификат?",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Сертификат удален",
"message": "Сертификат успешно удален"
},
"error": {
"title": "",
"message": ""
"title": "Не удалось удалить сертификат",
"message": "Произошла ошибка при удалении сертификата"
}
}
}
+63
View File
@@ -733,6 +733,14 @@
"password": {
"label": "Heslo",
"newLabel": "Nové heslo"
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": "Skopírovať URL"
},
"open": {
"label": ""
}
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": "Zobraziť informácie o súborovom systéme"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": "Nepodarilo sa načítať zdravotný stav"
},
"cluster": {
"summary": {
"cpu": "",
"memory": ""
},
"resource": {
"node": {
"name": ""
},
"qemu": {
"name": ""
},
"lxc": {
"name": ""
},
"storage": {
"name": ""
}
},
"popover": {
"rightSection": {
"node": "",
"vmId": "",
"plugin": ""
},
"detail": {
"cpu": "",
"memory": "",
"storage": "",
"uptime": "",
"haState": "",
"storageType": {
"local": "",
"shared": ""
}
}
},
"table": {
"header": {
"name": "",
"cpu": "",
"memory": "",
"node": ""
}
}
}
},
"common": {
+63
View File
@@ -733,6 +733,14 @@
"password": {
"label": "Geslo",
"newLabel": "Novo geslo"
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": ""
},
"open": {
"label": ""
}
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": ""
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": ""
},
"cluster": {
"summary": {
"cpu": "",
"memory": ""
},
"resource": {
"node": {
"name": ""
},
"qemu": {
"name": ""
},
"lxc": {
"name": ""
},
"storage": {
"name": ""
}
},
"popover": {
"rightSection": {
"node": "",
"vmId": "",
"plugin": ""
},
"detail": {
"cpu": "",
"memory": "",
"storage": "",
"uptime": "",
"haState": "",
"storageType": {
"local": "",
"shared": ""
}
}
},
"table": {
"header": {
"name": "",
"cpu": "",
"memory": "",
"node": ""
}
}
}
},
"common": {
+63
View File
@@ -733,6 +733,14 @@
"password": {
"label": "Lösenord",
"newLabel": "Nytt lösenord"
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": ""
},
"open": {
"label": ""
}
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": "Visa information om filsystemet"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": ""
},
"cluster": {
"summary": {
"cpu": "",
"memory": ""
},
"resource": {
"node": {
"name": ""
},
"qemu": {
"name": ""
},
"lxc": {
"name": ""
},
"storage": {
"name": ""
}
},
"popover": {
"rightSection": {
"node": "",
"vmId": "",
"plugin": ""
},
"detail": {
"cpu": "",
"memory": "",
"storage": "",
"uptime": "",
"haState": "",
"storageType": {
"local": "",
"shared": ""
}
}
},
"table": {
"header": {
"name": "",
"cpu": "",
"memory": "",
"node": ""
}
}
}
},
"common": {
+85 -22
View File
@@ -712,7 +712,7 @@
"label": "Değer ayarlanmadı",
"tooltip": "Gizli anahtar gereklidir ve henüz ayarlanmadı"
},
"secureNotice": "Bu Gizli anahtar oluşturulduktan sonra geri alınamaz",
"secureNotice": "Entegrasyonu oluşturduktan sonra gizli anahtarı tekrar görüntüleyemezsiniz",
"reset": {
"title": "Gizli anahtarı sıfırla",
"message": "Gizli anahtarı sıfırlamak istediğinizden emin misiniz?"
@@ -733,6 +733,14 @@
"password": {
"label": "Şifre",
"newLabel": "Yeni Şifre"
},
"tokenId": {
"label": "Token Kimliği",
"newLabel": "Yeni Token Kimliği"
},
"realm": {
"label": "Realm",
"newLabel": "Yeni realm"
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": "URL'yi kopyala"
},
"open": {
"label": ""
}
}
},
@@ -897,7 +908,7 @@
"passwordRequirements": "Parola gereksinimleri karşılamıyor",
"boardAlreadyExists": "Bu isimde bir panel zaten mevcut",
"invalidFileType": "Geçersiz dosya türü, {expected} bekleniyor",
"invalidFileName": "",
"invalidFileName": "Geçersiz dosya adı",
"fileTooLarge": "Dosya çok büyük, azami boyut {maxSize}",
"invalidConfiguration": "Geçersiz yapılandırma",
"groupNameTaken": "Grup adı zaten alınmış"
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": "Dosya Sistemi Bilgilerini Göster"
},
"defaultTab": {
"label": "Varsayılan sekme"
},
"sectionIndicatorRequirement": {
"label": "Bölüm göstergesi gereksinimi"
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": "Sağlık durumu alınamadı"
},
"cluster": {
"summary": {
"cpu": "İşlemci",
"memory": "BELLEK"
},
"resource": {
"node": {
"name": "Nodes"
},
"qemu": {
"name": "VM's"
},
"lxc": {
"name": "LXC'ler"
},
"storage": {
"name": "Depolama"
}
},
"popover": {
"rightSection": {
"node": "Node",
"vmId": "VM Kimliği",
"plugin": "Eklenti"
},
"detail": {
"cpu": "Çekirdekler",
"memory": "Hafıza",
"storage": "Depolama",
"uptime": "Çalışma Süresi",
"haState": "HA Durumu",
"storageType": {
"local": "Yerel depolama",
"shared": "Paylaşılan depolama"
}
}
},
"table": {
"header": {
"name": "İsim",
"cpu": "İşlemci",
"memory": "BELLEK",
"node": "Node"
}
}
}
},
"common": {
@@ -1673,7 +1736,7 @@
"main": "Medya İstatistikleri",
"approved": "Onaylanan",
"pending": "Onay bekleyen",
"processing": "İşleniyor",
"processing": "İşlenen",
"declined": "Reddedilen",
"available": "Mevcut",
"tv": "Dizi talepleri",
@@ -2039,9 +2102,9 @@
"error": {
"noBoard": {
"title": "Homarr'a Hoş Geldiniz",
"description": "Tüm uygulamalarınızı ve hizmetlerinizi parmaklarınızın ucuna getiren şık ve modern bir gösterge paneli.",
"description": "Tüm uygulamalarınızı ve hizmetlerinizi parmaklarınızın ucuna getiren şık ve modern kontrol paneli.",
"link": "İlk panelinizi oluşturun",
"notice": "Bu sayfanın kaybolmasını sağlamak için bir panel oluşturun ve bunu ana panel olarak ayarlayın"
"notice": "Bu sayfanın kaybolmasını sağlamak için bir panel oluşturun ve bunu ön tanımlı panel olarak atayın"
},
"notFound": {
"title": "Panel bulunamadı",
@@ -2102,7 +2165,7 @@
"docker": "Docker",
"logs": "Günlükler",
"api": "API",
"certificates": "",
"certificates": "Sertifikalar",
"tasks": "Görevler"
}
},
@@ -2710,7 +2773,7 @@
"label": "Günlükler"
},
"certificates": {
"label": ""
"label": "Sertifikalar"
}
},
"settings": {
@@ -3110,39 +3173,39 @@
"certificate": {
"page": {
"list": {
"title": "",
"description": "",
"title": "Güvenilen sertifikalar",
"description": "Homarr tarafından, Entegrasyonlardan veri çekmek için kullanılır.",
"noResults": {
"title": ""
"title": "Henüz sertifika yok"
},
"expires": ""
"expires": "{when} Sonra Süresi Doluyor"
}
},
"action": {
"create": {
"label": "",
"label": "Sertifika ekle",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Sertifika eklendi",
"message": "Sertifika başarıyla eklendi"
},
"error": {
"title": "",
"message": ""
"title": "Sertifika eklenemedi",
"message": "Sertifika eklenemedi"
}
}
},
"remove": {
"label": "",
"confirm": "",
"label": "Sertifikayı kaldır",
"confirm": "Sertifikayı kaldırmak istediğinizden emin misiniz?",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Sertifika kaldırıldı",
"message": "Sertifika başarıyla kaldırıldı"
},
"error": {
"title": "",
"message": ""
"title": "Sertifika kaldırılmadı",
"message": "Sertifika kaldırılamadı"
}
}
}
+249 -186
View File
@@ -316,19 +316,19 @@
"item": {
"create": {
"label": "Створення додатків",
"description": ""
"description": "Дозволити учасникам створювати додатки"
},
"use-all": {
"label": "",
"description": ""
"label": "Використовувати всі додатки",
"description": "Дозволити учасникам додавати будь-які додатки до своїх дошок"
},
"modify-all": {
"label": "",
"description": ""
"label": "Змінювати всі додатки",
"description": "Дозволити учасникам змінювати всі додатки"
},
"full-all": {
"label": "",
"description": ""
"label": "Повний доступ до додатка",
"description": "Дозволити учасникам керувати, використовувати і видаляти будь-які додатки"
}
}
},
@@ -336,84 +336,84 @@
"title": "Дошки",
"item": {
"create": {
"label": "",
"description": ""
"label": "Створити дошки",
"description": "Дозволити учасникам створювати дошки"
},
"view-all": {
"label": "",
"description": ""
"label": "Переглядати всі дошки",
"description": "Дозволити учасникам переглядати всі дошки"
},
"modify-all": {
"label": "",
"description": ""
"label": "Змінювати всі дошки",
"description": "Дозволити учасникам змінювати всі дошки (не включає контроль доступу та небезпечну зону)"
},
"full-all": {
"label": "",
"description": ""
"label": "Повний доступ до дошки",
"description": "Дозволити учасникам переглядати, змінювати та видаляти всі дошки (включаючи контроль доступу та небезпечну зону)"
}
}
},
"integration": {
"title": "",
"title": "Інтеграції",
"item": {
"create": {
"label": "",
"description": ""
"label": "Створювати інтеграції",
"description": "Дозволити учасникам створювати інтеграції"
},
"use-all": {
"label": "",
"description": ""
"label": "Використовувати всі інтеграції",
"description": "Дозволяє учасникам додавати будь-які інтеграції до своїх дошок"
},
"interact-all": {
"label": "",
"description": ""
"label": "Взаємодіяти з будь-якою інтеграцією",
"description": "Дозволити учасникам взаємодіяти з будь-якою інтеграцією"
},
"full-all": {
"label": "",
"description": ""
"label": "Повний доступ до інтеграції",
"description": "Дозволити учасникам керувати, використовувати та взаємодіяти з будь-якою інтеграцією"
}
}
},
"media": {
"title": "",
"title": "Медіа",
"item": {
"upload": {
"label": "",
"description": ""
"label": "Завантажити медіафайли",
"description": "Дозволити учасникам завантажувати медіафайли"
},
"view-all": {
"label": "",
"description": ""
"label": "Переглядати всі медіафайли",
"description": "Дозволити учасникам переглядати всі медіафайли"
},
"full-all": {
"label": "",
"description": ""
"label": "Повний доступ до медіафайлів",
"description": "Дозволити учасникам керувати та видаляти будь-які медіафайли"
}
}
},
"other": {
"title": "",
"title": "Інше",
"item": {
"view-logs": {
"label": "",
"description": ""
"label": "Переглядати логи",
"description": "Дозволити учасникам переглядати логи"
}
}
},
"search-engine": {
"title": "",
"title": "Пошукові системи",
"item": {
"create": {
"label": "",
"description": ""
"label": "Створення пошукових систем",
"description": "Дозволити учасникам створювати пошукові системи"
},
"modify-all": {
"label": "",
"description": ""
"label": "Змінити всі пошукові системи",
"description": "Дозволити учасникам змінювати всі пошукові системи"
},
"full-all": {
"label": "",
"description": ""
"label": "Повний доступ до пошукових систем",
"description": "Дозволити учасникам керувати та видаляти будь-які пошукові системи"
}
}
}
@@ -423,24 +423,24 @@
"external": ""
},
"reservedNotice": {
"message": ""
"message": "Ця група зарезервована для використання системою та обмежує деякі дії. <checkoutDocs></checkoutDocs>"
},
"action": {
"create": {
"label": "",
"label": "Нова група",
"notification": {
"success": {
"message": ""
"message": "Групу успішно створено"
},
"error": {
"message": ""
"message": "Не вдалося створити групу"
}
}
},
"transfer": {
"label": "",
"description": "",
"confirm": "",
"confirm": "Ви впевнені, що бажаєте передати право власності на групу {name} користувачу {username}?",
"notification": {
"success": {
"message": "Група {group} перенесена до {user}"
@@ -499,7 +499,7 @@
}
},
"app": {
"search": "",
"search": "Пошук додатків",
"page": {
"list": {
"title": "Додатки",
@@ -594,25 +594,25 @@
"notification": {
"success": {
"title": "Зміни застосовано",
"message": ""
"message": "Інтеграція успішно збережена"
},
"error": {
"title": "",
"message": ""
"title": "Не вдалося застосувати зміни",
"message": "Не вдалося зберегти інтеграцію"
}
}
},
"delete": {
"title": "",
"message": "",
"title": "Видалити інтеграцію",
"message": "Ви впевнені, що хочете видалити інтеграцію {name}?",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Успішно видалено",
"message": "Інтеграцію успішно видалено"
},
"error": {
"title": "",
"message": ""
"title": "Не вдалося видалити",
"message": "Не вдалося видалити інтеграцію"
}
}
}
@@ -625,23 +625,23 @@
"label": ""
},
"attemptSearchEngineCreation": {
"label": "",
"label": "Створення пошукових систем",
"description": ""
}
},
"action": {
"create": ""
"create": "Нова інтеграція"
},
"testConnection": {
"action": {
"create": "",
"edit": ""
"create": "Перевірити підключення та створити",
"edit": "Перевірити підключення та зберегти"
},
"alertNotice": "",
"notification": {
"success": {
"title": "",
"message": ""
"title": "З'єднання встановлено",
"message": "З'єднання успішно встановлено"
},
"invalidUrl": {
"title": "Неправильна URL-адреса",
@@ -656,11 +656,11 @@
"message": ""
},
"commonError": {
"title": "",
"message": ""
"title": "Помилка підключення",
"message": "Не вдалося встановити з'єднання"
},
"badRequest": {
"title": "",
"title": "Невірний запит",
"message": ""
},
"unauthorized": {
@@ -668,28 +668,28 @@
"message": ""
},
"forbidden": {
"title": "",
"message": ""
"title": "Заборонено",
"message": "Ймовірно, відсутні дозволи"
},
"notFound": {
"title": "",
"title": "Не знайдено",
"message": ""
},
"internalServerError": {
"title": "",
"title": "Внутрішня помилка сервера",
"message": ""
},
"serviceUnavailable": {
"title": "",
"message": ""
"message": "Сервер зараз недоступний"
},
"connectionAborted": {
"title": "",
"message": ""
},
"domainNotFound": {
"title": "",
"message": ""
"title": "Домен не знайдено",
"message": "Домен не знайдено"
},
"connectionRefused": {
"title": "",
@@ -733,6 +733,14 @@
"password": {
"label": "Пароль",
"newLabel": "Новий пароль"
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -744,7 +752,7 @@
},
"media": {
"plural": "",
"search": "",
"search": "Пошук медіафайлів",
"field": {
"name": "Ім’я",
"size": "Розмір",
@@ -753,7 +761,7 @@
"action": {
"upload": {
"label": "",
"file": "",
"file": "Виберіть файл",
"notification": {
"success": {
"message": ""
@@ -777,11 +785,14 @@
},
"copy": {
"label": ""
},
"open": {
"label": ""
}
}
},
"common": {
"beta": "",
"beta": "Бета-версія",
"error": "Помилка",
"action": {
"add": "Додати",
@@ -789,7 +800,7 @@
"backToOverview": "",
"create": "Створити",
"edit": "Редагувати",
"import": "",
"import": "Імпорт",
"insert": "Вставити",
"remove": "Видалити",
"save": "Зберегти",
@@ -798,13 +809,13 @@
"delete": "Видалити",
"discard": "",
"confirm": "Підтвердити",
"continue": "",
"continue": "Продовжити",
"previous": "Попередній",
"next": "Далі",
"checkoutDocs": "",
"checkLogs": "",
"checkLogs": "Щоб дізнатися більше, перевірте логи",
"tryAgain": "Повторіть спробу",
"loading": ""
"loading": "Завантаження"
},
"here": "",
"iconPicker": {
@@ -813,29 +824,29 @@
},
"colorScheme": {
"options": {
"light": "",
"dark": ""
"light": "Світла",
"dark": "Темна"
}
},
"information": {
"min": "",
"max": "",
"days": "",
"hours": "",
"minutes": ""
"days": "Днів",
"hours": "Годин",
"minutes": "Хвилин"
},
"notification": {
"create": {
"success": "",
"error": ""
"success": "Успішно створено",
"error": "Не вдалося створити"
},
"delete": {
"success": "",
"error": ""
"success": "Успішно видалено",
"error": "Не вдалося видалити"
},
"update": {
"success": "",
"error": ""
"success": "Зміни успішно застосовано",
"error": "Не вдалося застосувати зміни"
},
"transfer": {
"success": "",
@@ -847,7 +858,7 @@
},
"multiText": {
"placeholder": "",
"addLabel": ""
"addLabel": "Додати {value}"
},
"select": {
"placeholder": "",
@@ -857,14 +868,14 @@
},
"userAvatar": {
"menu": {
"switchToDarkMode": "",
"switchToLightMode": "",
"management": "",
"switchToDarkMode": "Перемикнути на темну тему",
"switchToLightMode": "Перемикнути на світлу тему",
"management": "Управління",
"preferences": "Ваші уподобання",
"logout": "",
"logout": "Вийти",
"login": "Логін",
"homeBoard": "",
"loggedOut": "",
"loggedOut": "Ви вийшли",
"updateAvailable": ""
}
},
@@ -893,9 +904,9 @@
"number": "Це поле повинно бути менше або дорівнювати {maximum}"
},
"custom": {
"passwordsDoNotMatch": "",
"passwordRequirements": "",
"boardAlreadyExists": "",
"passwordsDoNotMatch": "Паролі не збігаються",
"passwordRequirements": "Пароль не відповідає вимогам",
"boardAlreadyExists": "Дошка з такою назвою вже існує",
"invalidFileType": "",
"invalidFileName": "",
"fileTooLarge": "",
@@ -923,29 +934,29 @@
}
},
"action": {
"create": "",
"edit": "",
"remove": "",
"create": "Нова категорія",
"edit": "Перейменувати категорію",
"remove": "Видалити категорію",
"moveUp": "Рухайся.",
"moveDown": "Вниз.",
"createAbove": "",
"createBelow": ""
},
"create": {
"title": "",
"submit": ""
"title": "Нова категорія",
"submit": "Додати категорію"
},
"remove": {
"title": "",
"message": ""
"title": "Видалити категорію",
"message": "Ви впевнені, що хочете видалити категорію {name}?"
},
"edit": {
"title": "",
"submit": ""
"title": "Перейменувати категорію",
"submit": "Перейменувати категорію"
},
"menu": {
"label": {
"create": "",
"create": "Нова категорія",
"changePosition": "Змінити положення"
}
}
@@ -1137,7 +1148,7 @@
},
"clock": {
"name": "",
"description": "Відображає поточну дату і час.",
"description": "Показує поточні дату і час.",
"option": {
"customTitleToggle": {
"label": "",
@@ -1193,7 +1204,7 @@
"description": "",
"option": {
"showToolbar": {
"label": "Показати панель інструментів для написання націнки"
"label": "Показати панель інструментів для допомоги з розміткою"
},
"allowReadOnlyCheck": {
"label": ""
@@ -1235,9 +1246,9 @@
"deleteRow": ""
},
"align": {
"left": "Ліворуч.",
"left": "Ліворуч",
"center": "Центр",
"right": "Так."
"right": "Праворуч"
},
"popover": {
"clearColor": "",
@@ -1250,7 +1261,7 @@
}
},
"iframe": {
"name": "IFrame",
"name": "iFrame",
"description": "Вставити будь-який контент з інтернету. Деякі вебсайти можуть обмежувати доступ.",
"option": {
"embedUrl": {
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": ""
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": ""
},
"cluster": {
"summary": {
"cpu": "",
"memory": ""
},
"resource": {
"node": {
"name": ""
},
"qemu": {
"name": ""
},
"lxc": {
"name": ""
},
"storage": {
"name": ""
}
},
"popover": {
"rightSection": {
"node": "",
"vmId": "",
"plugin": ""
},
"detail": {
"cpu": "",
"memory": "",
"storage": "",
"uptime": "",
"haState": "",
"storageType": {
"local": "",
"shared": ""
}
}
},
"table": {
"header": {
"name": "",
"cpu": "",
"memory": "",
"node": ""
}
}
}
},
"common": {
@@ -1776,16 +1839,16 @@
"board": {
"action": {
"duplicate": {
"title": "",
"message": "",
"title": "Скопіювати дошку",
"message": "Це продублює дошку {name} з усім її вмістом. Якщо віджети посилаються на інтеграції, які ви не можете використовувати, їх буде видалено.",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Дошку скопійовано",
"message": "Дошку успішно продубльовано"
},
"error": {
"title": "",
"message": ""
"title": "Неможливо продублювати дошку",
"message": "Дошку не може бути продубльовано"
}
}
},
@@ -1806,7 +1869,7 @@
}
},
"oldImport": {
"label": "",
"label": "Імпорт з homarr до версії 1.0.0",
"notification": {
"success": {
"title": "",
@@ -1825,8 +1888,8 @@
"apps": {
"label": "Додатки",
"avoidDuplicates": {
"label": "",
"description": ""
"label": "Уникати дублікатів",
"description": "Ігнорує додатки, якщо додаток з таким самим посиланням вже існує"
},
"onlyImportApps": {
"label": "",
@@ -2108,12 +2171,12 @@
},
"settings": "Налаштування",
"help": {
"label": "Допоможіть!",
"label": "Допомога",
"items": {
"documentation": "Документація",
"submitIssue": "",
"discord": "Розбрат у громаді",
"sourceCode": ""
"submitIssue": "Повідомити про проблему",
"discord": "Discord",
"sourceCode": "Вихідний код"
}
},
"about": "Про програму"
@@ -2140,30 +2203,30 @@
"title": "Твої дошки",
"action": {
"new": {
"label": ""
"label": "Нова дошка"
},
"open": {
"label": ""
"label": "Відкрити дошку"
},
"settings": {
"label": "Налаштування"
},
"setHomeBoard": {
"label": "",
"label": "Встановити як домашню дошку",
"badge": {
"label": "Головна",
"tooltip": ""
"tooltip": "Ця дошка буде вашою домашньою дошкою"
}
},
"setMobileHomeBoard": {
"label": "",
"label": "Встановити як мобільну дошку",
"badge": {
"label": "",
"tooltip": ""
"tooltip": "Ця дошка буде вашою мобільною дошкою"
}
},
"duplicate": {
"label": ""
"label": "Скопіювати дошку"
},
"delete": {
"label": "Видалити назавжди",
@@ -2314,81 +2377,81 @@
"title": "Налаштування",
"notification": {
"success": {
"message": ""
"message": "Налаштування успішно збережено"
},
"error": {
"message": ""
"message": "Не вдалося зберегти налаштування"
}
},
"section": {
"analytics": {
"title": "",
"title": "Аналітика",
"general": {
"title": "",
"text": ""
"title": "Надсилати анонімну аналітику",
"text": "Homarr буде відправляти анонімну аналітику з використанням програмного забезпечення з відкритим вихідним кодом Umami. Він ніколи не збирає жодної особистої інформації і, таким чином, сумісний з GDPR та CCPA. Ми заохочуємо вас увімкнути аналітику, тому що це допомагає нашій команді визначати проблеми та пріоритетні задачі."
},
"widgetData": {
"title": "",
"text": ""
"title": "Дані віджетів",
"text": "Надсилати, які віджети (і їх кількість) ви налаштували. Не містить URL, назви, або будь-які інші дані."
},
"integrationData": {
"title": "",
"text": ""
"title": "Дані інтеграції",
"text": "Надсилати, які віджети (і їх кількість) ви налаштували. Не містить URL, назви, або будь-які інші дані."
},
"usersData": {
"title": "",
"text": ""
"title": "Дані користувачів",
"text": "Надсилати кількість користувачів і чи активували ви SSO"
}
},
"crawlingAndIndexing": {
"title": "",
"warning": "",
"title": "Пошукові сканери та індексація",
"warning": "Увімкнення або вимкнення будь-яких налаштувань тут серйозно вплине на те, як пошукові системи індексуватимуть і скануватимуть вашу сторінку. Будь-яке налаштування є запитом, і пошуковий сканер має застосувати ці налаштування. Будь-які зміни можуть зайняти кілька днів або тижнів. Деякі налаштування можуть залежати від пошукової системи.",
"noIndex": {
"title": "",
"text": ""
"title": "Не індексувати (No Index)",
"text": "Не індексувати сайт у пошукових системах і не показувати його в жодному результаті пошуку"
},
"noFollow": {
"title": "",
"text": ""
"title": "Не переходити (No follow)",
"text": "Не переходьте за посиланнями під час індексації. Якщо вимкнути це, то сканери намагатимуться переходити за всіма посиланнями на Homarr."
},
"noTranslate": {
"title": "",
"text": ""
"title": "Не перекладати (No translate)",
"text": "Якщо мова сайту не є тією, яку користувач, імовірно, захоче прочитати, Google покаже посилання на переклад у результатах пошуку"
},
"noSiteLinksSearchBox": {
"title": "",
"text": ""
"title": "Без вікна пошуку з посиланнями на сайт",
"text": "Google створює вікно пошуку зі просканованими посиланнями разом з іншими прямими посиланнями. Ввімкнення цього налаштування просить Google відключити таке вікно пошуку."
}
},
"board": {
"title": "Дошки",
"homeBoard": {
"label": "",
"mobileLabel": "",
"description": ""
"label": "Глобальна домашня дошка",
"mobileLabel": "Глобальна мобільна дошка",
"description": "Тут доступні тільки публічні дошки"
}
},
"search": {
"title": "",
"title": "Пошук",
"defaultSearchEngine": {
"label": "",
"description": ""
"label": "Глобальна система пошуку за замовчуванням",
"description": "Пошукові системи з інтеграцій тут недоступні"
}
},
"appearance": {
"title": "Вигляд",
"defaultColorScheme": {
"label": "",
"label": "Стандартна кольорова тема",
"options": {
"light": "",
"dark": ""
"light": "Світла",
"dark": "Темна"
}
}
},
"culture": {
"title": "",
"title": "Культура",
"defaultLocale": {
"label": ""
"label": "Стандартна мова"
}
}
}
@@ -2485,20 +2548,20 @@
}
},
"about": {
"version": "",
"text": "",
"version": "Версія {version}",
"text": "Homarr — це проєкт із відкритим кодом, керований спільнотою, який підтримується волонтерами. Завдяки цим людям проєкт Homarr розвивається з 2021 року. Наша команда працює над Homarr абсолютно віддалено з різних країн у вільний час без жодної компенсації.",
"accordion": {
"contributors": {
"title": "",
"subtitle": ""
"title": "Учасники",
"subtitle": "{count} - стільки людей розробляє код та Homarr"
},
"translators": {
"title": "",
"subtitle": ""
"title": "Перекладачі",
"subtitle": "{count} - стільки людей допомагають перекладати багатьма мовами"
},
"libraries": {
"title": "",
"subtitle": ""
"title": "Бібліотеки",
"subtitle": "{count} - стільки бібліотек використано в коді Homarr"
}
}
}
@@ -2729,14 +2792,14 @@
},
"mode": {
"appIntegrationBoard": {
"help": "",
"help": "Пошук додатків, інтеграцій, або дошок",
"group": {
"app": {
"title": "Додатки",
"children": {
"action": {
"open": {
"label": ""
"label": "Відкрити посилання на додаток"
},
"edit": {
"label": ""
@@ -2752,7 +2815,7 @@
"children": {
"action": {
"open": {
"label": ""
"label": "Відкрити дошку"
},
"homeBoard": {
"label": ""
@@ -2761,7 +2824,7 @@
"label": ""
},
"settings": {
"label": ""
"label": "Відкрити налаштування"
}
},
"detail": {
@@ -2796,7 +2859,7 @@
}
},
"newBoard": {
"label": ""
"label": "Створити нову дошку"
},
"importBoard": {
"label": ""
@@ -2831,7 +2894,7 @@
"openIn": ""
},
"external": {
"help": "",
"help": "Використовувати зовнішню пошукову систему",
"group": {
"searchEngine": {
"title": "",
@@ -2876,19 +2939,19 @@
"help": {
"group": {
"mode": {
"title": ""
"title": "Режими"
},
"help": {
"title": "Допоможіть!",
"title": "Допомога",
"option": {
"documentation": {
"label": "Документація"
},
"submitIssue": {
"label": ""
"label": "Повідомити про проблему"
},
"discord": {
"label": "Розбрат у громаді"
"label": "Discord"
}
}
}
@@ -2920,7 +2983,7 @@
}
},
"page": {
"help": "",
"help": "Пошук сторінок",
"group": {
"page": {
"title": "",
@@ -2981,7 +3044,7 @@
}
},
"userGroup": {
"help": "",
"help": "Пошук користувачів або груп",
"group": {
"user": {
"title": "Користувачі",
+63
View File
@@ -733,6 +733,14 @@
"password": {
"label": "Mật khẩu",
"newLabel": "Mật khẩu mới"
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": ""
},
"open": {
"label": ""
}
}
},
@@ -1411,6 +1422,12 @@
},
"fileSystem": {
"label": ""
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1447,52 @@
"memory": {},
"error": {
"internalServerError": ""
},
"cluster": {
"summary": {
"cpu": "",
"memory": ""
},
"resource": {
"node": {
"name": ""
},
"qemu": {
"name": ""
},
"lxc": {
"name": ""
},
"storage": {
"name": ""
}
},
"popover": {
"rightSection": {
"node": "",
"vmId": "",
"plugin": ""
},
"detail": {
"cpu": "",
"memory": "",
"storage": "",
"uptime": "",
"haState": "",
"storageType": {
"local": "",
"shared": ""
}
}
},
"table": {
"header": {
"name": "",
"cpu": "",
"memory": "",
"node": ""
}
}
}
},
"common": {
Loaded 100 of 113 files, more files were not shown because too many files have changed in this diff. Show more