Compare commits

...
70 Commits
Author SHA1 Message Date
Releases Homarr b4b19f486e chore(release): 1.2.0 [skip ci] 2025-01-24 19:12:53 +00:00
homarr-releases[bot] 32cf09a62e chore(release): automatic release v1.2.0 2025-01-24 19:11:55 +00:00
Meier Lukas a1ba4372da fix: system health monitoring gets proxmox id passed (#2094) 2025-01-24 17:36:57 +01:00
Meier Lukas 4e153b8046 fix: url-encode nzbget credentials (#2093) 2025-01-24 17:36:37 +01:00
Meier Lukas ac788650fb fix: issue with foreign key constraint do to empty string for home board (#2088) 2025-01-24 17:36:27 +01:00
homarr-crowdin[bot] a9b0e26c8d chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-24 00:44:37 +00:00
Yossi Hillali 132db15424 feat: add bookmarks options (apps) (#2048) 2025-01-23 22:10:16 +02:00
homarr-renovate[bot] 860b73fa3f chore(deps): update dependency @types/node to ^22.10.10 (#2091)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-23 18:20:56 +00:00
Yossi Hillali e4dd19b574 fix: date & time widget padding cut off (#2089) 2025-01-23 19:25:44 +02:00
homarr-renovate[bot] 1b4ed02010 chore(deps): update dependency @types/ws to ^8.5.14 (#2090)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-23 16:55:45 +00:00
homarr-renovate[bot] 47b7cbb086 chore(deps): update node.js to v22.13.1 (#2068)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-23 17:47:14 +01:00
homarr-renovate[bot] 7a6477a272 chore(deps): update vitest monorepo to ^3.0.4 (#2082)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-23 16:19:41 +00:00
homarr-renovate[bot] bff536008c chore(deps): update dependency @types/react to v19.0.8 (#2081)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-23 15:54:12 +00:00
homarr-renovate[bot] 8b040eddef fix(deps): update turbo monorepo to ^2.3.4 (#2085)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-23 15:45:42 +00:00
homarr-crowdin[bot] ce87d62b35 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-23 00:43:53 +00:00
Meier Lukas f88cc7381b chore(issue-template): add missing homarr versions 2025-01-22 21:00:59 +01:00
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
122 changed files with 6042 additions and 3011 deletions

No files matched your search

+4 -1
View File
@@ -31,6 +31,9 @@ body:
label: Version
description: What version of Homarr are you running?
options:
- 1.1.0
- 1.0.1
- 1.0.0
- 1.0.0-beta
- Other (describe in "additional information")
default: 0
@@ -75,4 +78,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
+58
View File
@@ -1,3 +1,61 @@
## [1.2.0](https://github.com/homarr-labs/homarr/compare/v1.1.0...v1.2.0) (2025-01-24)
### Features
* add bookmarks options (apps) ([#2048](https://github.com/homarr-labs/homarr/issues/2048)) ([132db15](https://github.com/homarr-labs/homarr/commit/132db154242959dc2f16add4e58b0bdb7b96fb8c))
### Bug Fixes
* date & time widget padding cut off ([#2089](https://github.com/homarr-labs/homarr/issues/2089)) ([e4dd19b](https://github.com/homarr-labs/homarr/commit/e4dd19b574bfe0dcbcd92a3c5993aef550683fc3))
* **deps:** update turbo monorepo to ^2.3.4 ([#2085](https://github.com/homarr-labs/homarr/issues/2085)) ([8b040ed](https://github.com/homarr-labs/homarr/commit/8b040eddef8dbc1269489038bc503dcf23ba9580))
* issue with foreign key constraint do to empty string for home board ([#2088](https://github.com/homarr-labs/homarr/issues/2088)) ([ac78865](https://github.com/homarr-labs/homarr/commit/ac788650fb60d7aca889ec270dc29edb7ee53123))
* system health monitoring gets proxmox id passed ([#2094](https://github.com/homarr-labs/homarr/issues/2094)) ([a1ba437](https://github.com/homarr-labs/homarr/commit/a1ba4372da3430777a85dc83bbfc59f4b03d65fb))
* url-encode nzbget credentials ([#2093](https://github.com/homarr-labs/homarr/issues/2093)) ([4e153b8](https://github.com/homarr-labs/homarr/commit/4e153b804664e4fbc63ee5eb945c8096c809c6d5))
## [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 -1
View File
@@ -1,4 +1,4 @@
FROM node:22.13.0-alpine AS base
FROM node:22.13.1-alpine AS base
FROM base AS builder
RUN apk add --no-cache libc6-compat
+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";
+18 -16
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",
@@ -83,9 +85,9 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "3.1.0",
"@types/node": "^22.10.7",
"@types/node": "^22.10.10",
"@types/prismjs": "^1.26.5",
"@types/react": "19.0.7",
"@types/react": "19.0.8",
"@types/react-dom": "19.0.3",
"@types/swagger-ui-react": "^4.18.3",
"concurrently": "^9.1.2",
@@ -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 (
@@ -39,8 +39,8 @@ export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormPro
});
const form = useZodForm(validation.user.changeHomeBoards, {
initialValues: {
homeBoardId: user.homeBoardId ?? "",
mobileHomeBoardId: user.mobileHomeBoardId ?? "",
homeBoardId: user.homeBoardId,
mobileHomeBoardId: user.mobileHomeBoardId,
},
});
@@ -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,
});
+2 -2
View File
@@ -38,13 +38,13 @@
"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",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^22.10.7",
"@types/node": "^22.10.10",
"dotenv-cli": "^8.0.0",
"eslint": "^9.18.0",
"prettier": "^3.4.2",
+1 -1
View File
@@ -33,7 +33,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/ws": "^8.5.13",
"@types/ws": "^8.5.14",
"eslint": "^9.18.0",
"prettier": "^3.4.2",
"typescript": "^5.7.3"
+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,
+9 -9
View File
@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "1.0.0",
"version": "1.2.0",
"private": true,
"scripts": {
"build": "cross-env CI=true turbo build",
@@ -38,29 +38,29 @@
"@semantic-release/github": "^11.0.1",
"@semantic-release/npm": "^12.0.1",
"@semantic-release/release-notes-generator": "^14.0.3",
"@turbo/gen": "^2.3.3",
"@turbo/gen": "^2.3.4",
"@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "^2.1.8",
"@vitest/ui": "^2.1.8",
"@vitest/coverage-v8": "^3.0.4",
"@vitest/ui": "^3.0.4",
"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",
"turbo": "^2.3.3",
"testcontainers": "^10.17.1",
"turbo": "^2.3.4",
"typescript": "^5.7.3",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^2.1.8"
"vitest": "^3.0.4"
},
"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": {
@@ -96,7 +96,7 @@ export class NzbGetIntegration extends DownloadClientIntegration {
): Promise<ReturnType<NzbGetClient[CallType]>> {
const username = this.getSecretValue("username");
const password = this.getSecretValue("password");
const url = this.url(`/${username}:${password}/jsonrpc`);
const url = this.url(`/${encodeURIComponent(username)}:${encodeURIComponent(password)}/jsonrpc`);
const body = JSON.stringify({ method, params });
return await fetchWithTrustedCertificatesAsync(url, { method: "POST", body })
.then(async (response) => {
+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);
+4 -1
View File
@@ -2,8 +2,8 @@ import { objectEntries } from "@homarr/common";
import { logger } from "@homarr/log";
import type { WidgetComponentProps } from "../../../widgets/src/definition";
import { mapKind } from "./definitions";
import type { InversedWidgetMapping, OldmarrWidgetDefinitions, WidgetMapping } from "./definitions";
import { mapKind } from "./definitions";
// This type enforces, that for all widget mappings there is a corresponding option mapping,
// each option of newmarr can be mapped from the value of the oldmarr options
@@ -38,6 +38,9 @@ const optionMapping: OptionMapping = {
return mappedLayouts[oldOptions.layout];
},
hideIcon: (oldOptions) => oldOptions.items.some((item) => item.hideIcon),
hideHostname: (oldOptions) => oldOptions.items.some((item) => item.hideHostname),
openNewTab: (oldOptions) => oldOptions.items.some((item) => item.openNewTab),
},
calendar: {
releaseType: (oldOptions) => [oldOptions.radarrReleaseType],
+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"
+90 -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": "用户组名称已存在"
@@ -1056,6 +1067,15 @@
}
}
},
"hideIcon": {
"label": ""
},
"hideHostname": {
"label": ""
},
"openNewTab": {
"label": ""
},
"items": {
"label": "书签",
"add": "添加书签"
@@ -1411,6 +1431,12 @@
},
"fileSystem": {
"label": "显示文件系统信息"
},
"defaultTab": {
"label": "預設頁面"
},
"sectionIndicatorRequirement": {
"label": "部分指示需求"
}
},
"popover": {
@@ -1430,6 +1456,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 +2174,7 @@
"docker": "Docker",
"logs": "日志",
"api": "API",
"certificates": "",
"certificates": "证书",
"tasks": "任务"
}
},
@@ -2710,7 +2782,7 @@
"label": "日志"
},
"certificates": {
"label": ""
"label": "证书"
}
},
"settings": {
@@ -3110,39 +3182,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": "无法删除证书"
}
}
}
+72
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": ""
}
}
},
@@ -1056,6 +1067,15 @@
}
}
},
"hideIcon": {
"label": ""
},
"hideHostname": {
"label": ""
},
"openNewTab": {
"label": ""
},
"items": {
"label": "",
"add": ""
@@ -1411,6 +1431,12 @@
},
"fileSystem": {
"label": "Zobrazit informace o souborovém systému"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1456,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
+72
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": ""
}
}
},
@@ -1056,6 +1067,15 @@
}
}
},
"hideIcon": {
"label": ""
},
"hideHostname": {
"label": ""
},
"openNewTab": {
"label": ""
},
"items": {
"label": "Lesezeichen",
"add": "Lesezeichen hinzufügen"
@@ -1411,6 +1431,12 @@
},
"fileSystem": {
"label": "Dateisystem Info anzeigen"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1456,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": {
+72
View File
@@ -733,6 +733,14 @@
"password": {
"label": "Κωδικός",
"newLabel": "Νέος κωδικός"
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": ""
},
"open": {
"label": ""
}
}
},
@@ -1056,6 +1067,15 @@
}
}
},
"hideIcon": {
"label": ""
},
"hideHostname": {
"label": ""
},
"openNewTab": {
"label": ""
},
"items": {
"label": "",
"add": ""
@@ -1411,6 +1431,12 @@
},
"fileSystem": {
"label": "Εμφάνιση Πληροφοριών Συστήματος Αρχείων"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1456,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": {
+16
View File
@@ -785,6 +785,9 @@
},
"copy": {
"label": "Copy URL"
},
"open": {
"label": "Open media"
}
}
},
@@ -1064,6 +1067,15 @@
}
}
},
"hideIcon": {
"label": "Hide icons"
},
"hideHostname": {
"label": "Hide hostnames"
},
"openNewTab": {
"label": "Open in new tab"
},
"items": {
"label": "Bookmarks",
"add": "Add bookmark"
@@ -1407,6 +1419,10 @@
"healthMonitoring": {
"name": "System Health Monitoring",
"description": "Displays information showing the health and status of your system(s).",
"tab": {
"system": "System",
"cluster": "Cluster"
},
"option": {
"fahrenheit": {
"label": "CPU Temp in Fahrenheit"
+72
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": ""
}
}
},
@@ -1056,6 +1067,15 @@
}
}
},
"hideIcon": {
"label": ""
},
"hideHostname": {
"label": ""
},
"openNewTab": {
"label": ""
},
"items": {
"label": "",
"add": ""
@@ -1411,6 +1431,12 @@
},
"fileSystem": {
"label": "Mostrar información del sistema de archivos"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1456,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": {
+72
View File
@@ -733,6 +733,14 @@
"password": {
"label": "",
"newLabel": ""
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": ""
},
"open": {
"label": ""
}
}
},
@@ -1056,6 +1067,15 @@
}
}
},
"hideIcon": {
"label": ""
},
"hideHostname": {
"label": ""
},
"openNewTab": {
"label": ""
},
"items": {
"label": "",
"add": ""
@@ -1411,6 +1431,12 @@
},
"fileSystem": {
"label": ""
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1456,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": {
+72
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": ""
}
}
},
@@ -1056,6 +1067,15 @@
}
}
},
"hideIcon": {
"label": ""
},
"hideHostname": {
"label": ""
},
"openNewTab": {
"label": ""
},
"items": {
"label": "",
"add": ""
@@ -1411,6 +1431,12 @@
},
"fileSystem": {
"label": "Afficher les infos sur le système de fichiers"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1456,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": {
+234 -162
View File
@@ -733,6 +733,14 @@
"password": {
"label": "סיסמה",
"newLabel": "סיסמה חדשה"
},
"tokenId": {
"label": "מזהה אסימון",
"newLabel": "מזהה אסימון חדש"
},
"realm": {
"label": "תחום",
"newLabel": "תחום חדש"
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": "העתק קישור"
},
"open": {
"label": "פתיחת מדיה"
}
}
},
@@ -897,7 +908,7 @@
"passwordRequirements": "הסיסמה אינה עומדת בדרישות",
"boardAlreadyExists": "לוח בשם זה כבר קיים",
"invalidFileType": "סוג קובץ לא חוקי, צפוי {expected}",
"invalidFileName": "",
"invalidFileName": "שם קובץ לא חוקי",
"fileTooLarge": "הקובץ גדול מדי, הגודל המרבי הוא {maxSize}",
"invalidConfiguration": "תצורה לא חוקית",
"groupNameTaken": "שם הקבוצה כבר תפוס"
@@ -1056,6 +1067,15 @@
}
}
},
"hideIcon": {
"label": ""
},
"hideHostname": {
"label": ""
},
"openNewTab": {
"label": ""
},
"items": {
"label": "סימניות",
"add": "הוסף סימניה"
@@ -1180,7 +1200,7 @@
"label": "כתובת השרת"
},
"isBedrockServer": {
"label": ""
"label": "שרת Bedrock"
}
},
"status": {
@@ -1411,6 +1431,12 @@
},
"fileSystem": {
"label": "הצג מידע על מערכת הקבצים"
},
"defaultTab": {
"label": "כרטיסיית ברירת מחדל"
},
"sectionIndicatorRequirement": {
"label": "דרישת מציין מקטע"
}
},
"popover": {
@@ -1430,6 +1456,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": {
@@ -2102,7 +2174,7 @@
"docker": "דוקר",
"logs": "קובצי לוג",
"api": "ממשק API",
"certificates": "",
"certificates": "אישורי אבטחה",
"tasks": "משימות"
}
},
@@ -2156,10 +2228,10 @@
}
},
"setMobileHomeBoard": {
"label": "",
"label": "הגדרה כלוח שלך בנייד",
"badge": {
"label": "",
"tooltip": ""
"label": "מכשיר נייד",
"tooltip": "לוח זה יופיע כלוח הנייד שלך"
}
},
"duplicate": {
@@ -2199,10 +2271,10 @@
"item": {
"language": "שפה ואיזור",
"board": {
"title": "",
"title": "לוח בית",
"type": {
"general": "",
"mobile": ""
"general": "כללי",
"mobile": "מכשיר נייד"
}
},
"defaultSearchEngine": "מנוע חיפוש ברירת מחדל",
@@ -2364,7 +2436,7 @@
"title": "לוחות",
"homeBoard": {
"label": "לוח בית גלובלי",
"mobileLabel": "",
"mobileLabel": "לוח גלובלי למכשיר נייד",
"description": "רק לוחות ציבוריים זמינים לבחירה"
}
},
@@ -2442,10 +2514,10 @@
"label": "ניטור בריאות המערכת"
},
"dnsHole": {
"label": ""
"label": "נתוני חור DNS"
},
"sessionCleanup": {
"label": ""
"label": "ניקוי סשן"
},
"updateChecker": {
"label": "בודק עדכונים"
@@ -2459,8 +2531,8 @@
"title": "ממשק API",
"modal": {
"createApiToken": {
"title": "",
"description": "",
"title": "נוצר אסימון API",
"description": "האסימון API נוצר. היזהר, אסימון זה מוצפן בבסיס הנתונים ולא יישלח אליך שוב. אם תאבד את האסימון הזה, לא תוכל לשחזר אותו בשנית.",
"button": "העתקה וסגירה"
}
},
@@ -2486,19 +2558,19 @@
},
"about": {
"version": "גרסה {version}",
"text": "",
"text": "Homarr הוא פרויקט קוד פתוח מונחה קהילה שמתוחזק על ידי מתנדבים. בזכות האנשים האלה, Homarr הוא פרויקט שצומח מאז 2021. הצוות שלנו עובד מרחוק ממדינות שונות בזמן הפנוי שלהם, ללא תמורה.",
"accordion": {
"contributors": {
"title": "",
"subtitle": ""
"title": "תורמים",
"subtitle": "{count} מתחזקי קוד & Homarr"
},
"translators": {
"title": "",
"subtitle": ""
"title": "מתרגמים",
"subtitle": "{count} תורמים בתרגום לשפות שונות"
},
"libraries": {
"title": "",
"subtitle": ""
"title": "ספריות",
"subtitle": "{count} בשימוש בקוד של Homarr"
}
}
}
@@ -2507,9 +2579,9 @@
"docker": {
"title": "מיכלים",
"table": {
"updated": "",
"search": "",
"selected": ""
"updated": "עודכן ב- {when}",
"search": "חפש {count} קונטיינרים",
"selected": "{selectCount} מ- {totalCount} קונטיינרים נבחרו"
},
"field": {
"name": {
@@ -2522,9 +2594,9 @@
"running": "פועל",
"paused": "מושהה",
"restarting": "מפעיל מחדש",
"exited": "",
"exited": "נסגר",
"removing": "מסיר",
"dead": ""
"dead": "מת"
}
},
"containerImage": {
@@ -2539,12 +2611,12 @@
"label": "התחל",
"notification": {
"success": {
"title": "",
"message": ""
"title": "קונטיינרים הופעלו",
"message": "הקונטיינרים הופעלו בהצלחה"
},
"error": {
"title": "",
"message": ""
"title": "הקונטיינרים לא הופעלו",
"message": "לא היה ניתן להפעיל את הקונטיינרים"
}
}
},
@@ -2552,12 +2624,12 @@
"label": "עצור",
"notification": {
"success": {
"title": "",
"message": ""
"title": "הקונטיינרים נעצרו",
"message": "הקונטיינרים נעצרו בהצלחה"
},
"error": {
"title": "",
"message": ""
"title": "הקונטיינרים לא נעצרו",
"message": "לא היה ניתן לעצור את הקונטיינרים"
}
}
},
@@ -2565,12 +2637,12 @@
"label": "אתחל",
"notification": {
"success": {
"title": "",
"message": ""
"title": "הקונטיינרים הופעלו מחדש",
"message": "הקונטיינרים הופעלו מחדש בהצלחה"
},
"error": {
"title": "",
"message": ""
"title": "הקונטיינרים לא הופעלו מחדש",
"message": "לא היה ניתן להפעיל את הקונטיינרים מחדש"
}
}
},
@@ -2578,61 +2650,61 @@
"label": "הסר",
"notification": {
"success": {
"title": "",
"message": ""
"title": "הקונטיינרים הוסרו",
"message": "הקונטיינרים הוסרו בהצלחה"
},
"error": {
"title": "",
"message": ""
"title": "הקונטיינרים לא הוסרו",
"message": "לא היה ניתן להסיר את הקונטיינרים"
}
}
},
"refresh": {
"label": "",
"label": "רענון",
"notification": {
"success": {
"title": "",
"message": ""
"title": "הקונטיינרים התרעננו",
"message": "כעת את/ה צופה בנתונים העדכניים ביותר"
},
"error": {
"title": "",
"message": ""
"title": "הקונטיינרים לא התרעננו",
"message": "משהו השתבש בזמן רענון הקונטיינרים"
}
}
},
"addToHomarr": {
"label": "",
"label": "הוסף ל- Homarr",
"notification": {
"success": {
"title": "",
"message": ""
"title": "הוסף ל- Homarr",
"message": "האפליקציות שנבחרו הוספו ל- Homarr"
},
"error": {
"title": "",
"message": ""
"title": "לא היה ניתן להוסיף ל- Homarr",
"message": "לא היה ניתן להוסיף את האפליקציות שנבחרו ל- Homarr"
}
},
"modal": {
"title": ""
"title": "הוספת דוקר קונטיינר/ים ל- Homarr"
}
}
},
"error": {
"internalServerError": ""
"internalServerError": "נכשל בקבלת קונטיינרי דוקר"
}
},
"permission": {
"title": "",
"title": "הרשאות",
"userSelect": {
"title": ""
"title": "הוספת הרשאת משתמש"
},
"groupSelect": {
"title": ""
"title": "הוספת הרשאת קבוצה"
},
"tab": {
"user": "משתמשים",
"group": "קבוצות",
"inherited": ""
"inherited": "קבוצות בירושה"
},
"field": {
"user": {
@@ -2710,7 +2782,7 @@
"label": "קבצי לוג"
},
"certificates": {
"label": ""
"label": "אישורי אבטחה"
}
},
"settings": {
@@ -2758,14 +2830,14 @@
"label": "קביעה כלוח הבית"
},
"mobileBoard": {
"label": ""
"label": "הגדרה כלוח שלך בנייד"
},
"settings": {
"label": "פתיחת הגדרות"
}
},
"detail": {
"title": ""
"title": "בחירת פעולה עבור הלוח"
}
}
},
@@ -2775,99 +2847,99 @@
}
},
"command": {
"help": "",
"help": "הפעלת מצב פקודות",
"group": {
"localCommand": {
"title": ""
"title": "פקודות מקומיות"
},
"globalCommand": {
"title": "",
"title": "פקודות גלובליות",
"option": {
"colorScheme": {
"light": "עבור למצב בהיר",
"dark": "עבור למצב כהה"
},
"language": {
"label": "",
"label": "שינוי שפה",
"children": {
"detail": {
"title": ""
"title": "בחירת שפה מועודפת"
}
}
},
"newBoard": {
"label": ""
"label": "יצירת לוח חדש"
},
"importBoard": {
"label": ""
"label": "ייבוא לוח"
},
"newApp": {
"label": ""
"label": "יצירת אפליקציה חדשה"
},
"newIntegration": {
"label": "",
"label": "יצירת אינטגרציה חדשה",
"children": {
"detail": {
"title": ""
"title": "בחירת סוג האינטגרציה שברצונך ליצור"
}
}
},
"newUser": {
"label": ""
"label": "יצירת משתמש חדש"
},
"newInvite": {
"label": ""
"label": "יצירת הזמנה חדשה"
},
"newGroup": {
"label": ""
"label": "יצירת קבוצה חדשה"
}
}
}
}
},
"media": {
"requestMovie": "",
"requestSeries": "",
"openIn": ""
"requestMovie": "בקשת סרט",
"requestSeries": "בקשת סדרה",
"openIn": "פתיחה ב- {kind}"
},
"external": {
"help": "",
"help": "שימוש במנוע חיפוש חיצוני",
"group": {
"searchEngine": {
"title": "מנועי חיפוש",
"children": {
"action": {
"search": {
"label": ""
"label": "חיפוש עם {name}"
}
},
"detail": {
"title": ""
"title": "בחירת פעולה עבור מנוע חיפוש"
},
"searchResults": {
"title": ""
"title": "בחירת תוצאה בחיפוש לביצוע פעולות"
}
},
"option": {
"google": {
"name": "",
"description": ""
"name": "גוגל",
"description": "חיפוש באינטרנט עם גוגל"
},
"bing": {
"name": "",
"description": ""
"name": "בינג",
"description": "חיפוש באינטרנט עם בינג"
},
"duckduckgo": {
"name": "",
"description": ""
"name": "DuckDuckGo",
"description": "חיפוש באינטרנט עם DuckDuckGo"
},
"torrent": {
"name": "טורנטים",
"description": ""
"description": "חיפוש טורנטים באמצעות torrentdownloads.pro"
},
"youTube": {
"name": "",
"description": ""
"name": "יוטיוב",
"description": "חיפוש וידאו ביוטיוב"
}
}
}
@@ -2876,7 +2948,7 @@
"help": {
"group": {
"mode": {
"title": ""
"title": "מצבים"
},
"help": {
"title": "עזרה",
@@ -2885,7 +2957,7 @@
"label": "תיעוד"
},
"submitIssue": {
"label": ""
"label": "שליחת בעיה"
},
"discord": {
"label": "קהילת דיסקורד"
@@ -2897,75 +2969,75 @@
"home": {
"group": {
"search": {
"title": "",
"title": "חיפוש",
"option": {
"other": {
"label": ""
"label": "חיפוש באמצעות מנוע חיפוש אחר"
},
"no-default": {
"label": "",
"description": ""
"label": "אין מנוע חיפוש ברירת מחדל",
"description": "הגדרת מנוע חיפוש ברירת מחדל בהעדפות"
},
"search": {
"label": ""
"label": "חיפוש {query} עם {name}"
},
"from-integration": {
"description": ""
"description": "התחל/י להקליד כדי לחפש"
}
}
},
"local": {
"title": ""
"title": "תוצאות מקומיות"
}
}
},
"page": {
"help": "",
"help": "חיפוש עבור דפים",
"group": {
"page": {
"title": "",
"title": "דפים",
"option": {
"manageHome": {
"label": ""
"label": "ניהול דף בית"
},
"manageBoard": {
"label": ""
"label": "ניהול לוחות"
},
"manageApp": {
"label": ""
"label": "ניהול אפליקציות"
},
"manageIntegration": {
"label": ""
"label": "ניהול אינטגרציות"
},
"manageSearchEngine": {
"label": ""
"label": "ניהול מנועי חיפוש"
},
"manageMedia": {
"label": ""
"label": "ניהול מדיה"
},
"manageUser": {
"label": "ניהול משתמשים"
},
"manageInvite": {
"label": ""
"label": "ניהול הזמנות"
},
"manageGroup": {
"label": ""
"label": "ניהול קבוצות"
},
"manageDocker": {
"label": ""
"label": "ניהול דוקר"
},
"manageApi": {
"label": ""
"label": "Swagger API"
},
"manageLog": {
"label": "הצג לוגים"
},
"manageTask": {
"label": ""
"label": "ניהול משימות"
},
"manageSettings": {
"label": ""
"label": "הגדרות כלליות"
},
"about": {
"label": "אודות"
@@ -2981,18 +3053,18 @@
}
},
"userGroup": {
"help": "",
"help": "חיפוש משתמשים או קבוצות",
"group": {
"user": {
"title": "משתמשים",
"children": {
"action": {
"detail": {
"label": ""
"label": "הצגת נתוני משתמש"
}
},
"detail": {
"title": ""
"title": "בחירת פעולה עבור משתנש"
}
}
},
@@ -3001,17 +3073,17 @@
"children": {
"action": {
"detail": {
"label": ""
"label": "הצגת נתוני קבוצה"
},
"manageMember": {
"label": ""
"label": "ניהול חברים"
},
"managePermission": {
"label": ""
"label": "ניהול הרשאות"
}
},
"detail": {
"title": ""
"title": "בחירת פעולה עבור הקבוצה"
}
}
}
@@ -3019,16 +3091,16 @@
}
},
"engine": {
"search": "",
"search": "חיפוש מנוע חיפוש",
"field": {
"name": {
"label": "שם"
},
"short": {
"label": ""
"label": "קצר"
},
"urlTemplate": {
"label": ""
"label": "תבנית חיפוש קישור אינטרנט"
},
"description": {
"label": "תיאור"
@@ -3038,53 +3110,53 @@
"list": {
"title": "מנועי חיפוש",
"noResults": {
"title": "",
"action": ""
"title": "עדיין אין מנוע חיפוש",
"action": "צור את מנוע החיפוש הראשון שלך"
},
"interactive": ""
"interactive": "אינטראקטיבי, עושה שימוש באינטגרציה"
},
"create": {
"title": "",
"title": "מנוע חיפוש חדש",
"notification": {
"success": {
"title": "",
"message": ""
"title": "מנוע חיפוש נוצר",
"message": "מנוע חיפוש נוצר בהצלחה"
},
"error": {
"title": "",
"message": ""
"title": "מנוע חיפוש לא נוצר",
"message": "לא היה ניתן ליצור את מנוע החיפוש"
}
}
},
"edit": {
"title": "",
"title": "עריכת מנוע חיפוש",
"notification": {
"success": {
"title": "השינויים הוחלו בהצלחה",
"message": ""
"message": "מנוע החיפוש נשמר בהצלחה"
},
"error": {
"title": "לא ניתן להחיל שינויים",
"message": ""
"message": "לא היה ניתן לשמור את מנוע החיפוש"
}
},
"configControl": "",
"configControl": "הגדרות",
"searchEngineType": {
"generic": "",
"fromIntegration": ""
"generic": "כללי",
"fromIntegration": "מאינטגרציה"
}
},
"delete": {
"title": "",
"message": "",
"title": "מחיקת מנוע חיפוש",
"message": "האם אתה בטוח/ה שברצונך למחוק את מנוע החיפוש {name}?",
"notification": {
"success": {
"title": "",
"message": ""
"title": "מנוע החיפוש נמחק",
"message": "מנוע החיפוש נמחק בהצלחה"
},
"error": {
"title": "",
"message": ""
"title": "מנוע החיפוש לא נמחק",
"message": "לא היה ניתן למחוק את מנוע החיפוש"
}
}
}
@@ -3092,15 +3164,15 @@
"media": {
"request": {
"modal": {
"title": "",
"title": "בקשה {name}",
"table": {
"header": {
"season": "",
"episodes": ""
"season": "עונה",
"episodes": "פרקים"
}
},
"button": {
"send": ""
"send": "שליחת בקשה"
}
}
}
@@ -3110,39 +3182,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": "לא היה ניתן להסיר את תעודת האבטחה"
}
}
}
+72
View File
@@ -733,6 +733,14 @@
"password": {
"label": "Lozinka",
"newLabel": ""
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": ""
},
"open": {
"label": ""
}
}
},
@@ -1056,6 +1067,15 @@
}
}
},
"hideIcon": {
"label": ""
},
"hideHostname": {
"label": ""
},
"openNewTab": {
"label": ""
},
"items": {
"label": "",
"add": ""
@@ -1411,6 +1431,12 @@
},
"fileSystem": {
"label": ""
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1456,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": {
+72
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": ""
}
}
},
@@ -1056,6 +1067,15 @@
}
}
},
"hideIcon": {
"label": ""
},
"hideHostname": {
"label": ""
},
"openNewTab": {
"label": ""
},
"items": {
"label": "Könyjelzők",
"add": "Könyvjelző hozzáadása"
@@ -1411,6 +1431,12 @@
},
"fileSystem": {
"label": "Fájlrendszer-információk megjelenítése"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1456,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": {
+72
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": ""
}
}
},
@@ -1056,6 +1067,15 @@
}
}
},
"hideIcon": {
"label": ""
},
"hideHostname": {
"label": ""
},
"openNewTab": {
"label": ""
},
"items": {
"label": "",
"add": ""
@@ -1411,6 +1431,12 @@
},
"fileSystem": {
"label": "Mostra Informazioni Filesystem"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1456,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": {
+72
View File
@@ -733,6 +733,14 @@
"password": {
"label": "パスワード",
"newLabel": "新しいパスワード"
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": ""
},
"open": {
"label": ""
}
}
},
@@ -1056,6 +1067,15 @@
}
}
},
"hideIcon": {
"label": ""
},
"hideHostname": {
"label": ""
},
"openNewTab": {
"label": ""
},
"items": {
"label": "",
"add": ""
@@ -1411,6 +1431,12 @@
},
"fileSystem": {
"label": "ファイルシステム情報を表示"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1456,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": {
+72
View File
@@ -733,6 +733,14 @@
"password": {
"label": "비밀번호",
"newLabel": ""
},
"tokenId": {
"label": "",
"newLabel": ""
},
"realm": {
"label": "",
"newLabel": ""
}
}
},
@@ -777,6 +785,9 @@
},
"copy": {
"label": ""
},
"open": {
"label": ""
}
}
},
@@ -1056,6 +1067,15 @@
}
}
},
"hideIcon": {
"label": ""
},
"hideHostname": {
"label": ""
},
"openNewTab": {
"label": ""
},
"items": {
"label": "",
"add": ""
@@ -1411,6 +1431,12 @@
},
"fileSystem": {
"label": ""
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1456,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": {
+72
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": ""
}
}
},
@@ -1056,6 +1067,15 @@
}
}
},
"hideIcon": {
"label": ""
},
"hideHostname": {
"label": ""
},
"openNewTab": {
"label": ""
},
"items": {
"label": "",
"add": ""
@@ -1411,6 +1431,12 @@
},
"fileSystem": {
"label": ""
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1456,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": {
+72
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": ""
}
}
},
@@ -1056,6 +1067,15 @@
}
}
},
"hideIcon": {
"label": ""
},
"hideHostname": {
"label": ""
},
"openNewTab": {
"label": ""
},
"items": {
"label": "",
"add": ""
@@ -1411,6 +1431,12 @@
},
"fileSystem": {
"label": "Rādīt failu sistēmas informāciju"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1456,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": {
+147 -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": {
@@ -1056,6 +1067,15 @@
}
}
},
"hideIcon": {
"label": ""
},
"hideHostname": {
"label": ""
},
"openNewTab": {
"label": ""
},
"items": {
"label": "Bladwijzers",
"add": "Bladwijzer toevoegen"
@@ -1094,7 +1114,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 +1190,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 +1303,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 +1431,12 @@
},
"fileSystem": {
"label": "Bestandssysteem info weergeven"
},
"defaultTab": {
"label": "Standaard tab"
},
"sectionIndicatorRequirement": {
"label": "Sectie indicator vereiste"
}
},
"popover": {
@@ -1430,6 +1456,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 +1569,9 @@
"description": "De huidige streams op je mediaservers weergeven",
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
"user": "Gebruiker",
"name": "Naam",
"id": "Id"
}
},
"downloads": {
@@ -1776,16 +1848,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 +2174,7 @@
"docker": "Docker",
"logs": "Logs",
"api": "API",
"certificates": "",
"certificates": "Certificaten",
"tasks": "Taken"
}
},
@@ -2156,14 +2228,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 +2271,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 +2436,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 +2475,7 @@
},
"job": {
"minecraftServerStatus": {
"label": ""
"label": "Minecraft server status"
},
"iconsUpdater": {
"label": "Icoon updater"
@@ -2601,19 +2673,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 +2782,7 @@
"label": "Logs"
},
"certificates": {
"label": ""
"label": "Certificaten"
}
},
"settings": {
@@ -2758,7 +2830,7 @@
"label": "Instellen als home-bord"
},
"mobileBoard": {
"label": ""
"label": "Instellen als mobiel bord"
},
"settings": {
"label": "Instellingen openen"
@@ -2826,9 +2898,9 @@
}
},
"media": {
"requestMovie": "",
"requestSeries": "",
"openIn": ""
"requestMovie": "Film aanvragen",
"requestSeries": "Series aanvragen",
"openIn": "Openen in {kind}"
},
"external": {
"help": "Gebruik een externe zoekmachine",
@@ -2897,20 +2969,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 +3164,15 @@
"media": {
"request": {
"modal": {
"title": "",
"title": "\"{name}\" aanvragen",
"table": {
"header": {
"season": "",
"episodes": ""
"season": "Seizoen",
"episodes": "Afleveringen"
}
},
"button": {
"send": ""
"send": "Aanvraag versturen"
}
}
}
@@ -3110,39 +3182,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"
}
}
}
+72
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": ""
}
}
},
@@ -1056,6 +1067,15 @@
}
}
},
"hideIcon": {
"label": ""
},
"hideHostname": {
"label": ""
},
"openNewTab": {
"label": ""
},
"items": {
"label": "",
"add": ""
@@ -1411,6 +1431,12 @@
},
"fileSystem": {
"label": "Vis filsysteminfo"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1456,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": {
+72
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": ""
}
}
},
@@ -1056,6 +1067,15 @@
}
}
},
"hideIcon": {
"label": ""
},
"hideHostname": {
"label": ""
},
"openNewTab": {
"label": ""
},
"items": {
"label": "Zakładki",
"add": "Dodaj zakładkę"
@@ -1411,6 +1431,12 @@
},
"fileSystem": {
"label": "Pokaż informacje o systemie plików"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1456,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": {
+122 -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": ""
}
}
},
@@ -1056,6 +1067,15 @@
}
}
},
"hideIcon": {
"label": ""
},
"hideHostname": {
"label": ""
},
"openNewTab": {
"label": ""
},
"items": {
"label": "",
"add": ""
@@ -1411,6 +1431,12 @@
},
"fileSystem": {
"label": "Mostrar informações do sistema de arquivos"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1456,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": {
+72
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": ""
}
}
},
@@ -1056,6 +1067,15 @@
}
}
},
"hideIcon": {
"label": ""
},
"hideHostname": {
"label": ""
},
"openNewTab": {
"label": ""
},
"items": {
"label": "",
"add": ""
@@ -1411,6 +1431,12 @@
},
"fileSystem": {
"label": "Afișare informații despre sistemul de fișiere"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1456,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": {
+148 -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": {
@@ -1056,6 +1067,15 @@
}
}
},
"hideIcon": {
"label": ""
},
"hideHostname": {
"label": ""
},
"openNewTab": {
"label": ""
},
"items": {
"label": "Закладки",
"add": "Добавить закладку"
@@ -1094,7 +1114,7 @@
"dnsQueriesToday": "DNS-запросов сегодня",
"domainsBeingBlocked": "Доменов в списке блокировки"
},
"domainsTooltip": ""
"domainsTooltip": "Из-за наличия нескольких интеграций Homarr не может рассчитать точное количество заблокированных доменов"
},
"dnsHoleControls": {
"name": "Управление DNS-фильтром",
@@ -1170,22 +1190,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 +1303,7 @@
},
"error": {
"noUrl": "URL для iFrame не указан",
"unsupportedProtocol": "",
"unsupportedProtocol": "Указанный URL использует неподдерживаемый протокол. Пожалуйста, используйте один из ({supportedProtocols})",
"noBrowerSupport": "Ваш браузер не поддерживает iframes. Пожалуйста, обновите браузер."
}
},
@@ -1411,6 +1431,12 @@
},
"fileSystem": {
"label": "Показать информацию о файловой системе"
},
"defaultTab": {
"label": "Вкладка по умолчанию"
},
"sectionIndicatorRequirement": {
"label": "Пороговое значение индикатора раздела"
}
},
"popover": {
@@ -1430,6 +1456,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 +1569,9 @@
"description": "Отображает текущие сеансы воспроизведения на медиасерверах",
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
"user": "Пользователь",
"name": "Название",
"id": "Id"
}
},
"downloads": {
@@ -1776,16 +1848,16 @@
"board": {
"action": {
"duplicate": {
"title": "",
"message": "",
"title": "Дублировать панель",
"message": "Это действие создаст копию панели {name} со всем её содержимым. Если виджеты используют интеграции, к которым у вас нет доступа, они будут удалены.",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Панель продублирована",
"message": "Панель успешно продублирована"
},
"error": {
"title": "",
"message": ""
"title": "Ошибка дублирования",
"message": "Не удалось продублировать панель"
}
}
},
@@ -2102,7 +2174,7 @@
"docker": "Docker",
"logs": "Логи",
"api": "API",
"certificates": "",
"certificates": "Сертификаты",
"tasks": "Задачи"
}
},
@@ -2156,14 +2228,14 @@
}
},
"setMobileHomeBoard": {
"label": "",
"label": "Сделать домашней панелью для мобильных устройств",
"badge": {
"label": "",
"tooltip": ""
"label": "Мобильная",
"tooltip": "Эта панель будет показываться как ваша домашняя панель на мобильных устройствах"
}
},
"duplicate": {
"label": ""
"label": "Дублировать"
},
"delete": {
"label": "Удалить навсегда",
@@ -2199,13 +2271,13 @@
"item": {
"language": "Язык и регион",
"board": {
"title": "",
"title": "Домашняя панель",
"type": {
"general": "",
"mobile": ""
"general": "Основная",
"mobile": "Мобильная"
}
},
"defaultSearchEngine": "",
"defaultSearchEngine": "Поисковая система по умолчанию",
"firstDayOfWeek": "Первый день недели",
"accessibility": "Специальные возможности"
}
@@ -2363,16 +2435,16 @@
"board": {
"title": "Панели",
"homeBoard": {
"label": "Общая домашняя панель",
"mobileLabel": "",
"label": "Глобальная домашняя панель",
"mobileLabel": "Глобальная мобильная панель",
"description": "Для выбора доступны только публичные панели"
}
},
"search": {
"title": "",
"title": "Поиск",
"defaultSearchEngine": {
"label": "",
"description": ""
"label": "Глобальная поисковая система",
"description": "Поисковые системы из интеграций нельзя выбрать здесь"
}
},
"appearance": {
@@ -2403,7 +2475,7 @@
},
"job": {
"minecraftServerStatus": {
"label": ""
"label": "Статус сервера Minecraft"
},
"iconsUpdater": {
"label": "Обновление иконок"
@@ -2601,19 +2673,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 +2782,7 @@
"label": "Логи"
},
"certificates": {
"label": ""
"label": "Сертификаты"
}
},
"settings": {
@@ -2758,7 +2830,7 @@
"label": "Сделать домашней панелью"
},
"mobileBoard": {
"label": ""
"label": "Сделать домашней панелью для мобильных устройств"
},
"settings": {
"label": "Открыть настройки"
@@ -2826,9 +2898,9 @@
}
},
"media": {
"requestMovie": "",
"requestSeries": "",
"openIn": ""
"requestMovie": "Запросить фильм",
"requestSeries": "Запросить сериал",
"openIn": "Открыть в {kind}"
},
"external": {
"help": "Использовать внешнюю поисковую систему",
@@ -2897,20 +2969,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 +3164,15 @@
"media": {
"request": {
"modal": {
"title": "",
"title": "Запрос \"{name}\"",
"table": {
"header": {
"season": "",
"episodes": ""
"season": "Сезон",
"episodes": "Серии"
}
},
"button": {
"send": ""
"send": "Отправить запрос"
}
}
}
@@ -3110,39 +3182,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": "Произошла ошибка при удалении сертификата"
}
}
}
+72
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": ""
}
}
},
@@ -1056,6 +1067,15 @@
}
}
},
"hideIcon": {
"label": ""
},
"hideHostname": {
"label": ""
},
"openNewTab": {
"label": ""
},
"items": {
"label": "Záložky",
"add": "Pridať záložku"
@@ -1411,6 +1431,12 @@
},
"fileSystem": {
"label": "Zobraziť informácie o súborovom systéme"
},
"defaultTab": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
},
"popover": {
@@ -1430,6 +1456,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": {
Loaded 100 of 122 files, more files were not shown because too many files have changed in this diff. Show more