Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95e7136284 | ||
|
|
9de9aa59a4 | ||
|
|
ee12461452 | ||
|
|
b03941ed80 | ||
|
|
968a82baf5 | ||
|
|
ea8bf43d7b | ||
|
|
964368a06c | ||
|
|
98ecc683e5 | ||
|
|
29ff879dfd | ||
|
|
674253262b | ||
|
|
f9aeb8ffe3 | ||
|
|
76b8b35cc1 | ||
|
|
8886380f0a | ||
|
|
0b41967ec3 | ||
|
|
1b0504198b | ||
|
|
3363eebddd | ||
|
|
85672d9cf1 | ||
|
|
3131082c77 | ||
|
|
f33b1cb5d0 | ||
|
|
4f32f11d8a | ||
|
|
466d0e0053 | ||
|
|
7c4b71af3d | ||
|
|
81d1231d8f | ||
|
|
858fe6e19a | ||
|
|
ff5c31342f | ||
|
|
807997ea76 | ||
|
|
9c12b18c6c | ||
|
|
3951d052de | ||
|
|
5975b53212 | ||
|
|
cc9522d981 | ||
|
|
7f77130978 | ||
|
|
243a97d637 | ||
|
|
4feeb8b173 | ||
|
|
b1a8012bc7 | ||
|
|
22b67188bc | ||
|
|
dd6e2c0f72 | ||
|
|
9db0016252 | ||
|
|
3e3505176a | ||
|
|
3687c4b3d6 | ||
|
|
944010a7b1 | ||
|
|
db13fd03c0 | ||
|
|
0b8a0098bd | ||
|
|
9a77b540bb | ||
|
|
c598579716 | ||
|
|
59b1630532 | ||
|
|
77044e8e0b | ||
|
|
0ec8f23b7c | ||
|
|
c69e331ded | ||
|
|
4273cd9fcd | ||
|
|
98515312a2 | ||
|
|
d1b14aca8b | ||
|
|
c6b4c9cbf2 | ||
|
|
19029f40b8 | ||
|
|
b6bc257726 | ||
|
|
fef50a53b1 | ||
|
|
94ff401d7d | ||
|
|
63328d125a | ||
|
|
457b970d8f | ||
|
|
8d69970562 | ||
|
|
8a071f9ab6 | ||
|
|
76b67ac8e1 | ||
|
|
eba738e5d6 | ||
|
|
996ec548c4 | ||
|
|
40e00987cb | ||
|
|
93c64baec2 | ||
|
|
b5b0b069d2 | ||
|
|
62c7955e48 | ||
|
|
217818573e | ||
|
|
70879ccb90 | ||
|
|
34eb9b366f | ||
|
|
ac77bdd84c | ||
|
|
70ea093eb6 | ||
|
|
08e0dd76ae | ||
|
|
0ed4c741c5 | ||
|
|
569ec6fe5c | ||
|
|
e7e80dbc7d | ||
|
|
b054bdd987 | ||
|
|
84f73d33a0 | ||
|
|
78b55202e7 | ||
|
|
7706f97228 | ||
|
|
a0ee558582 | ||
|
|
94d3dad909 | ||
|
|
1a3a55934d | ||
|
|
8bf893b392 | ||
|
|
43b8cfa4dd | ||
|
|
a05b5965b6 | ||
|
|
68407eaa19 | ||
|
|
6854e4729d | ||
|
|
f03e381aea | ||
|
|
151ccb7086 | ||
|
|
8f7f2110ec | ||
|
|
579dd5763d | ||
|
|
3e1c000d51 | ||
|
|
f624611540 | ||
|
|
07f0abf28c | ||
|
|
c3264b2fe1 | ||
|
|
d0aa48fab3 | ||
|
|
d634fa2c95 | ||
|
|
5a8405a9a8 | ||
|
|
a6f388d076 | ||
|
|
1dd7b1a6b9 | ||
|
|
b7a6d950db | ||
|
|
00f5a877a6 | ||
|
|
75a949eecf | ||
|
|
ce0705e9d4 | ||
|
|
0f4a9af57d | ||
|
|
703a362356 | ||
|
|
826d54a0ca | ||
|
|
46c95e0da3 | ||
|
|
0aacbe95c2 | ||
|
|
18f845b4c4 | ||
|
|
b5ae5f876b | ||
|
|
ef359b6997 | ||
|
|
1d3dc5b9e7 | ||
|
|
ff0b0c659c | ||
|
|
8dee8f8680 | ||
|
|
25e4c814ac | ||
|
|
3edfe6bfed | ||
|
|
22d32ccb8d | ||
|
|
7aa5d2f8ed | ||
|
|
91a69c162a | ||
|
|
e3fcfbe916 | ||
|
|
3e4d24b28e | ||
|
|
425d5edab5 | ||
|
|
b0381cb883 | ||
|
|
a04cb25dd0 | ||
|
|
b4692b62ab | ||
|
|
bb8cb6dded | ||
|
|
4cc37397c2 | ||
|
|
2d2ae5a419 | ||
|
|
15197897ac | ||
|
|
289c7f1a60 | ||
|
|
495432073f | ||
|
|
c9fecaabb6 | ||
|
|
5b49fe1039 | ||
|
|
7ba2067689 | ||
|
|
e0ae5e6705 | ||
|
|
305d259e98 | ||
|
|
fed3d1ce23 | ||
|
|
8f083adf39 | ||
|
|
3e9c8172ef | ||
|
|
f743c76b17 |
No files matched your search
@@ -31,6 +31,10 @@ body:
|
||||
label: Version
|
||||
description: What version of Homarr are you running?
|
||||
options:
|
||||
- 1.13.1
|
||||
- 1.13.0
|
||||
- 1.12.0
|
||||
- 1.11.0
|
||||
- 1.10.0
|
||||
- 1.9.0
|
||||
- 1.8.0
|
||||
|
||||
@@ -6,11 +6,6 @@
|
||||
matchPackagePatterns: ["^@homarr/"],
|
||||
enabled: false,
|
||||
},
|
||||
// 15.2.0 crashes with turbopack error (panic)
|
||||
{
|
||||
matchPackagePatterns: ["^next$", "^@next/eslint-plugin-next$"],
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
matchUpdateTypes: ["minor", "patch", "pin", "digest"],
|
||||
automerge: true,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="All Tests" type="JavaScriptTestRunnerVitest" nameIsGenerated="true">
|
||||
<node-interpreter value="project" />
|
||||
<vitest-package value="$PROJECT_DIR$/node_modules/vitest" />
|
||||
<working-dir value="$PROJECT_DIR$" />
|
||||
<vitest-options value="--exclude e2e --coverage.enabled" />
|
||||
<envs>
|
||||
<env name="NODE_ENV" value="development" />
|
||||
<env name="CI" value="true" />
|
||||
</envs>
|
||||
<scope-kind value="ALL" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
+4
-4
@@ -12,17 +12,17 @@ languageIds:
|
||||
# You should unescape RegEx strings in order to fit in the YAML file
|
||||
# To help with this, you can use https://www.freeformatter.com/json-escape.html
|
||||
usageMatchRegex:
|
||||
# The following example shows how to detect `t("your.i18n.keys")`
|
||||
# the `{key}` will be placed by a proper keypath matching regex,
|
||||
# you can ignore it and use your own matching rules as well
|
||||
# For direct t("your.i18n.keys") usage
|
||||
- "[^\\w\\d]t\\(['\"`]({key})['\"`]"
|
||||
# For variable t assigned from getScopedI18n or useScopedI18n
|
||||
- "\\bt\\(['\"`]({key})['\"`]\\)"
|
||||
|
||||
# A RegEx to set a custom scope range. This scope will be used as a prefix when detecting keys
|
||||
# and works like how the i18next framework identifies the namespace scope from the
|
||||
# useTranslation() hook.
|
||||
# You should unescape RegEx strings in order to fit in the YAML file
|
||||
# To help with this, you can use https://www.freeformatter.com/json-escape.html
|
||||
scopeRangeRegex: "(getScopedI18n|useScopedI18n)\\(\\s*['\"](.*?)['\"]\\)"
|
||||
scopeRangeRegex: "(?:const|let|var)\\s+t\\s*=\\s*(?:await\\s+)?(?:getScopedI18n|useScopedI18n)\\(\\s*['\"](.*?)['\"]\\)"
|
||||
|
||||
# An array of strings containing refactor templates.
|
||||
# The "$1" will be replaced by the keypath specified.
|
||||
|
||||
+117
@@ -1,3 +1,120 @@
|
||||
## [1.14.0](https://github.com/homarr-labs/homarr/compare/v1.13.1...v1.14.0) (2025-04-04)
|
||||
|
||||
### Features
|
||||
|
||||
* **cli:** add cli command to recreate credentials admin ([#2779](https://github.com/homarr-labs/homarr/issues/2779)) ([6742532](https://github.com/homarr-labs/homarr/commit/674253262bd4853ebde85d229d666a38733a7026))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency @ctrl/qbittorrent to ^9.5.2 ([#2795](https://github.com/homarr-labs/homarr/issues/2795)) ([ea8bf43](https://github.com/homarr-labs/homarr/commit/ea8bf43d7b56f526fbf2d4a3955c2c179e4e74d8))
|
||||
* **deps:** update dependency @kubernetes/client-node to ^1.1.1 ([#2793](https://github.com/homarr-labs/homarr/issues/2793)) ([76b8b35](https://github.com/homarr-labs/homarr/commit/76b8b35cc1f586797210bdcd3d848d64b5f36d63))
|
||||
* **deps:** update dependency dockerode to ^4.0.5 ([#2757](https://github.com/homarr-labs/homarr/issues/2757)) ([db13fd0](https://github.com/homarr-labs/homarr/commit/db13fd03c018cb2f79f0b094b93b69d69fde97ff))
|
||||
* **deps:** update dependency eslint-plugin-react to ^7.37.5 ([#2794](https://github.com/homarr-labs/homarr/issues/2794)) ([f9aeb8f](https://github.com/homarr-labs/homarr/commit/f9aeb8ffe3ff4cd65cd76c560a687912262a735f))
|
||||
* **deps:** update dependency recharts to ^2.15.2 ([#2785](https://github.com/homarr-labs/homarr/issues/2785)) ([f33b1cb](https://github.com/homarr-labs/homarr/commit/f33b1cb5d0c317df5348e87e0f5f43fdc6044615))
|
||||
* **deps:** update dependency sass to ^1.86.1 ([#2762](https://github.com/homarr-labs/homarr/issues/2762)) ([3e35051](https://github.com/homarr-labs/homarr/commit/3e3505176a7df01bae5173d7f62380fa17859e8f))
|
||||
* **deps:** update dependency sass to ^1.86.2 ([#2784](https://github.com/homarr-labs/homarr/issues/2784)) ([3131082](https://github.com/homarr-labs/homarr/commit/3131082c778191be5568a93bbf5f10219f3c2be9))
|
||||
* **deps:** update dependency sass to ^1.86.3 ([#2796](https://github.com/homarr-labs/homarr/issues/2796)) ([98ecc68](https://github.com/homarr-labs/homarr/commit/98ecc683e563475644530682f08d335ac3671a5e))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.20.3 ([#2777](https://github.com/homarr-labs/homarr/issues/2777)) ([81d1231](https://github.com/homarr-labs/homarr/commit/81d1231d8fc026dd9f1ba58e28937aba9d94d88e))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.20.4 ([#2786](https://github.com/homarr-labs/homarr/issues/2786)) ([85672d9](https://github.com/homarr-labs/homarr/commit/85672d9cf15bf48c9cc2fb4e746f65814f46361b))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.20.5 ([#2787](https://github.com/homarr-labs/homarr/issues/2787)) ([3363eeb](https://github.com/homarr-labs/homarr/commit/3363eebdddf10a028d1cdebcb4d581105ba1d63c))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.20.6 ([#2798](https://github.com/homarr-labs/homarr/issues/2798)) ([968a82b](https://github.com/homarr-labs/homarr/commit/968a82baf5d6adc64c680508493b77b892fdd5fc))
|
||||
* **deps:** update dependency trpc-to-openapi to ^2.1.4 ([#2748](https://github.com/homarr-labs/homarr/issues/2748)) ([c69e331](https://github.com/homarr-labs/homarr/commit/c69e331ded3cfde837dcf28c8b1ba4a9b7a3c8aa))
|
||||
* **deps:** update dependency trpc-to-openapi to ^2.1.5 ([#2799](https://github.com/homarr-labs/homarr/issues/2799)) ([b03941e](https://github.com/homarr-labs/homarr/commit/b03941ed80057154f0b4b04c764652ada1fdf18c))
|
||||
* **deps:** update dependency typescript-eslint to ^8.29.0 ([#2761](https://github.com/homarr-labs/homarr/issues/2761)) ([3687c4b](https://github.com/homarr-labs/homarr/commit/3687c4b3d655d9cebb85fb3c3ad80f494e9a8f0a))
|
||||
* **deps:** update dependency undici to v7.7.0 ([#2776](https://github.com/homarr-labs/homarr/issues/2776)) ([ff5c313](https://github.com/homarr-labs/homarr/commit/ff5c31342fe9eb668533d7d9f0d65ad3f01eba0a))
|
||||
* **deps:** update tanstack-query monorepo to ^5.71.0 ([#2752](https://github.com/homarr-labs/homarr/issues/2752)) ([77044e8](https://github.com/homarr-labs/homarr/commit/77044e8e0b8e7267b8547eb1d0f86026948b292a))
|
||||
* **deps:** update tanstack-query monorepo to ^5.71.1 ([#2756](https://github.com/homarr-labs/homarr/issues/2756)) ([0b8a009](https://github.com/homarr-labs/homarr/commit/0b8a0098bd02a71107af9702e39000227028ec20))
|
||||
* **deps:** update tanstack-query monorepo to ^5.71.10 ([#2790](https://github.com/homarr-labs/homarr/issues/2790)) ([964368a](https://github.com/homarr-labs/homarr/commit/964368a06c8385df63a252e2d0759a2524348ac6))
|
||||
* **deps:** update tanstack-query monorepo to ^5.71.3 ([#2767](https://github.com/homarr-labs/homarr/issues/2767)) ([858fe6e](https://github.com/homarr-labs/homarr/commit/858fe6e19a682bb0fa0cfa0d11c2423447fdad2f))
|
||||
* **deps:** update tanstack-query monorepo to ^5.71.5 ([#2780](https://github.com/homarr-labs/homarr/issues/2780)) ([466d0e0](https://github.com/homarr-labs/homarr/commit/466d0e00530903ab4101cd2e94c00ab8a7c1c8ce))
|
||||
* **deps:** update testcontainers-node monorepo to ^10.24.0 ([#2789](https://github.com/homarr-labs/homarr/issues/2789)) ([1b05041](https://github.com/homarr-labs/homarr/commit/1b0504198b41032319ead2604721abaaf08709d8))
|
||||
* **deps:** update tiptap monorepo to v2.11.7 ([#2749](https://github.com/homarr-labs/homarr/issues/2749)) ([0ec8f23](https://github.com/homarr-labs/homarr/commit/0ec8f23b7c655889d8f6fb9fcfd66a9736eb5a10))
|
||||
* **deps:** update tiptap monorepo to v2.11.7 ([#2750](https://github.com/homarr-labs/homarr/issues/2750)) ([59b1630](https://github.com/homarr-labs/homarr/commit/59b16305326ac811ad91e89c0786b9e6b1b7e1df))
|
||||
* **deps:** update trpc monorepo to ^11.0.2 ([#2792](https://github.com/homarr-labs/homarr/issues/2792)) ([8886380](https://github.com/homarr-labs/homarr/commit/8886380f0a9ea7d35cd54182a378b2f47b71c283))
|
||||
* **deps:** update turbo monorepo to ^2.5.0 ([#2791](https://github.com/homarr-labs/homarr/issues/2791)) ([0b41967](https://github.com/homarr-labs/homarr/commit/0b41967ec366d925dbb172d38c3677d88f51f0a9))
|
||||
* **nextcloud:** issue with self signed certificates ([#2760](https://github.com/homarr-labs/homarr/issues/2760)) ([7f77130](https://github.com/homarr-labs/homarr/commit/7f77130978eb7f25e571b7315250c91656a85d78))
|
||||
* update check blocks page loading ([#2782](https://github.com/homarr-labs/homarr/issues/2782)) ([29ff879](https://github.com/homarr-labs/homarr/commit/29ff879dfd1036d44b7c4735a0b269a83173b4d1))
|
||||
|
||||
### Reverts
|
||||
|
||||
* add restriction callback to restrict visibility and modification of widget kinds ([#2746](https://github.com/homarr-labs/homarr/issues/2746)) ([9851531](https://github.com/homarr-labs/homarr/commit/98515312a2439d1ed9fc5b412843ca625120b2ac))
|
||||
|
||||
## [1.13.1](https://github.com/homarr-labs/homarr/compare/v1.13.0...v1.13.1) (2025-03-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update react monorepo to v19.1.0 ([#2734](https://github.com/homarr-labs/homarr/issues/2734)) ([94ff401](https://github.com/homarr-labs/homarr/commit/94ff401d7dface924f48affccbc59e3a98ed8dc1))
|
||||
|
||||
### Reverts
|
||||
|
||||
* ssg integration create ([#2736](https://github.com/homarr-labs/homarr/issues/2736)) ([fef50a5](https://github.com/homarr-labs/homarr/commit/fef50a53b1fb592668dfa9aade6b110e42aec9a8))
|
||||
|
||||
## [1.13.0](https://github.com/homarr-labs/homarr/compare/v1.12.0...v1.13.0) (2025-03-28)
|
||||
|
||||
### Features
|
||||
|
||||
* add default search engines seeding / set homarr docs as global server search engine ([#2663](https://github.com/homarr-labs/homarr/issues/2663)) ([3e1c000](https://github.com/homarr-labs/homarr/commit/3e1c000d515c0edc6f49241fc381187e1829ca7e))
|
||||
* add german swiss locale ([#2726](https://github.com/homarr-labs/homarr/issues/2726)) ([eba738e](https://github.com/homarr-labs/homarr/commit/eba738e5d6594d46b5b7d1eab875fc8060e0fe7b))
|
||||
* add jetbrains run profile ([#2704](https://github.com/homarr-labs/homarr/issues/2704)) ([07f0abf](https://github.com/homarr-labs/homarr/commit/07f0abf28cb31f81860e623c70970e16d17e2836))
|
||||
* add title to dynamic section ([#2614](https://github.com/homarr-labs/homarr/issues/2614)) ([1dd7b1a](https://github.com/homarr-labs/homarr/commit/1dd7b1a6b97f75bca0d1a5c0dbb4254aa4bbd418))
|
||||
* app dropdown add infinity scroll ([#2696](https://github.com/homarr-labs/homarr/issues/2696)) ([8bf893b](https://github.com/homarr-labs/homarr/commit/8bf893b39292f34f5c697c09ba2c9e53c8a6ea90))
|
||||
* **auth:** add env variable to force user-info usage instead of idtoken ([#2711](https://github.com/homarr-labs/homarr/issues/2711)) ([94d3dad](https://github.com/homarr-labs/homarr/commit/94d3dad909c39cec096ff61c0a2f45e8853b6111))
|
||||
* auto sign-in when creating the initial user ([#2664](https://github.com/homarr-labs/homarr/issues/2664)) ([46c95e0](https://github.com/homarr-labs/homarr/commit/46c95e0da3ad27eb072efc45250abb0baf3e2483))
|
||||
* implement auto-select icon ([#2679](https://github.com/homarr-labs/homarr/issues/2679)) ([1a3a559](https://github.com/homarr-labs/homarr/commit/1a3a55934d8b3e4703e0ce44d9d26ad7ff100aca))
|
||||
* **lang:** add english uk language ([#2715](https://github.com/homarr-labs/homarr/issues/2715)) ([93c64ba](https://github.com/homarr-labs/homarr/commit/93c64baec294c39cd490a85f8b06b57d53fc82b3))
|
||||
* **logs:** improve logs by logging errors with causes and metadata ([#2703](https://github.com/homarr-labs/homarr/issues/2703)) ([579dd57](https://github.com/homarr-labs/homarr/commit/579dd5763d139e1644c2e192620728afa54c96d2))
|
||||
* set home board when creating first board ([#2683](https://github.com/homarr-labs/homarr/issues/2683)) ([8f7f211](https://github.com/homarr-labs/homarr/commit/8f7f2110ec29db12ea2d83cd779ec86851209188))
|
||||
* SSG integration creation page ([#2684](https://github.com/homarr-labs/homarr/issues/2684)) ([ce0705e](https://github.com/homarr-labs/homarr/commit/ce0705e9d4a502273a0d84e4e14d61a061fd21f9))
|
||||
* **widget:** add restriction callback to restrict visibility and modification of widget kinds ([#2658](https://github.com/homarr-labs/homarr/issues/2658)) ([84f73d3](https://github.com/homarr-labs/homarr/commit/84f73d33a061a1d243803078b245a8016a615af9))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow deletion of external users ([#2725](https://github.com/homarr-labs/homarr/issues/2725)) ([40e0098](https://github.com/homarr-labs/homarr/commit/40e00987cb6f4ee58fdce1da9489eedd92b8843d))
|
||||
* **auth:** fusion auth returns www-authenticate token response ([#2719](https://github.com/homarr-labs/homarr/issues/2719)) ([b5b0b06](https://github.com/homarr-labs/homarr/commit/b5b0b069d2029110d5618a29af5ae003030584d9))
|
||||
* bookmarks layout ([#2675](https://github.com/homarr-labs/homarr/issues/2675)) ([8a071f9](https://github.com/homarr-labs/homarr/commit/8a071f9ab6bc4c85073dc52cc2e60da69f30950a))
|
||||
* calendar indicators ([#2712](https://github.com/homarr-labs/homarr/issues/2712)) ([70ea093](https://github.com/homarr-labs/homarr/commit/70ea093eb6530d25d3e60e6d496651e3fc457826))
|
||||
* **deps:** update dependency drizzle-kit to ^0.30.6 ([#2706](https://github.com/homarr-labs/homarr/issues/2706)) ([f03e381](https://github.com/homarr-labs/homarr/commit/f03e381aea414c5fb770c7ba092b31f9c53eee11))
|
||||
* **deps:** update dependency drizzle-zod to ^0.7.1 ([#2717](https://github.com/homarr-labs/homarr/issues/2717)) ([569ec6f](https://github.com/homarr-labs/homarr/commit/569ec6fe5ccec980351162024170fabe06dcc3da))
|
||||
* **deps:** update dependency ldapts to v7.3.3 ([#2691](https://github.com/homarr-labs/homarr/issues/2691)) ([00f5a87](https://github.com/homarr-labs/homarr/commit/00f5a877a6f09037eff4c9934fc273c4d2af0f6c))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.20.2 ([#2708](https://github.com/homarr-labs/homarr/issues/2708)) ([68407ea](https://github.com/homarr-labs/homarr/commit/68407eaa195894db37018cefeeca4ae2da085847))
|
||||
* **deps:** update dependency typescript-eslint to ^8.28.0 ([#2693](https://github.com/homarr-labs/homarr/issues/2693)) ([f624611](https://github.com/homarr-labs/homarr/commit/f6246115405cdc85948059170742b0bae58ab337))
|
||||
* **deps:** update dependency undici to v7.6.0 ([#2710](https://github.com/homarr-labs/homarr/issues/2710)) ([43b8cfa](https://github.com/homarr-labs/homarr/commit/43b8cfa4dd250013fdf15d509a072ba0984f9ab1))
|
||||
* **deps:** update mantine monorepo to ^7.17.3 ([#2707](https://github.com/homarr-labs/homarr/issues/2707)) ([6854e47](https://github.com/homarr-labs/homarr/commit/6854e4729d3ba4a82a318813fec0a20eccdbed18))
|
||||
* **deps:** update nextjs monorepo to v15.2.4 ([#2720](https://github.com/homarr-labs/homarr/issues/2720)) ([ac77bdd](https://github.com/homarr-labs/homarr/commit/ac77bdd84cba47a43d1952725a9c7feccd972766))
|
||||
* **deps:** update tanstack-query monorepo to ^5.69.2 ([#2714](https://github.com/homarr-labs/homarr/issues/2714)) ([78b5520](https://github.com/homarr-labs/homarr/commit/78b55202e7f4739933768d27211556f927b04fed))
|
||||
* **deps:** update tanstack-query monorepo to ^5.69.3 ([#2716](https://github.com/homarr-labs/homarr/issues/2716)) ([e7e80db](https://github.com/homarr-labs/homarr/commit/e7e80dbc7d62830ce1ce150b6e22a67220da8be8))
|
||||
* **deps:** update tanstack-query monorepo to ^5.70.0 ([#2722](https://github.com/homarr-labs/homarr/issues/2722)) ([34eb9b3](https://github.com/homarr-labs/homarr/commit/34eb9b366fdc58c9fc03d18ac7dd68bff1ea83e6))
|
||||
* **deps:** update testcontainers-node monorepo to ^10.22.0 ([#2692](https://github.com/homarr-labs/homarr/issues/2692)) ([b7a6d95](https://github.com/homarr-labs/homarr/commit/b7a6d950db557ea9deadc0be014b5f218611ac83))
|
||||
* **deps:** update testcontainers-node monorepo to ^10.23.0 ([#2699](https://github.com/homarr-labs/homarr/issues/2699)) ([c3264b2](https://github.com/homarr-labs/homarr/commit/c3264b2fe10ad374e92eb16921d43427d7ca866b))
|
||||
* **deps:** update tiptap monorepo to v2.11.6 ([#2718](https://github.com/homarr-labs/homarr/issues/2718)) ([0ed4c74](https://github.com/homarr-labs/homarr/commit/0ed4c741c59c3e498aa247adc292bdbbde1c59d3))
|
||||
* **deps:** update trpc monorepo to ^11.0.1 ([#2724](https://github.com/homarr-labs/homarr/issues/2724)) ([2178185](https://github.com/homarr-labs/homarr/commit/217818573e8bd7d49a821afdf269cd8b3f3d442d))
|
||||
* **import:** autofix missing shapes for sidebars and some sections as well ([#2723](https://github.com/homarr-labs/homarr/issues/2723)) ([62c7955](https://github.com/homarr-labs/homarr/commit/62c7955e48ca62b4bf1baf26ac6a12847795ac18))
|
||||
* Media request via search engine ([#2677](https://github.com/homarr-labs/homarr/issues/2677)) ([5a8405a](https://github.com/homarr-labs/homarr/commit/5a8405a9a81257b023c0265309f7f530b2e23c7a))
|
||||
|
||||
### Reverts
|
||||
|
||||
* add development package and update workspace ([#2700](https://github.com/homarr-labs/homarr/issues/2700))" ([#2728](https://github.com/homarr-labs/homarr/issues/2728)) ([996ec54](https://github.com/homarr-labs/homarr/commit/996ec548c4b9c8561919409b81adc90b0a24fbd6))
|
||||
|
||||
## [1.12.0](https://github.com/homarr-labs/homarr/compare/v1.11.0...v1.12.0) (2025-03-21)
|
||||
|
||||
### Features
|
||||
|
||||
* stock price widget ([#2579](https://github.com/homarr-labs/homarr/issues/2579)) ([91a69c1](https://github.com/homarr-labs/homarr/commit/91a69c162a51cff8899eaa7d5a113f6ec8feafba))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cli:** fix-usernames command only fixes lowercase usernames ([#2650](https://github.com/homarr-labs/homarr/issues/2650)) ([b0381cb](https://github.com/homarr-labs/homarr/commit/b0381cb8836b8477d0990c264aa399cb06b85237))
|
||||
* **deps:** update dependency better-sqlite3 to ^11.9.1 ([#2632](https://github.com/homarr-labs/homarr/issues/2632)) ([289c7f1](https://github.com/homarr-labs/homarr/commit/289c7f1a60477024207261d486c2a88718320df7))
|
||||
* **deps:** update dependency drizzle-orm to ^0.40.1 ([#2624](https://github.com/homarr-labs/homarr/issues/2624)) ([7ba2067](https://github.com/homarr-labs/homarr/commit/7ba2067689ed7fd3ab78e75a6ba385483a97b0a3))
|
||||
* **deps:** update dependency drizzle-orm to ^0.41.0 ([#2666](https://github.com/homarr-labs/homarr/issues/2666)) ([8dee8f8](https://github.com/homarr-labs/homarr/commit/8dee8f8680f2282d76f6e727123220a60e2b596d))
|
||||
* **deps:** update dependency mysql2 to v3.14.0 ([#2652](https://github.com/homarr-labs/homarr/issues/2652)) ([3e4d24b](https://github.com/homarr-labs/homarr/commit/3e4d24b28e998af80c00c8ad141660d4bfe5ef33))
|
||||
* **deps:** update dependency sass to ^1.86.0 ([#2629](https://github.com/homarr-labs/homarr/issues/2629)) ([c9fecaa](https://github.com/homarr-labs/homarr/commit/c9fecaabb6416db5c55d9066ed782712cc96faa0))
|
||||
* **deps:** update dependency typescript-eslint to ^8.27.0 ([#2653](https://github.com/homarr-labs/homarr/issues/2653)) ([e3fcfbe](https://github.com/homarr-labs/homarr/commit/e3fcfbe9166834e9443c5ead21c2f67b2d01ad2e))
|
||||
* **deps:** update tanstack-query monorepo to ^5.69.0 ([#2634](https://github.com/homarr-labs/homarr/issues/2634)) ([2d2ae5a](https://github.com/homarr-labs/homarr/commit/2d2ae5a4191085404f8f7f790a4f0f62b86b408d))
|
||||
* **deps:** update testcontainers-node monorepo to ^10.21.0 ([#2633](https://github.com/homarr-labs/homarr/issues/2633)) ([1519789](https://github.com/homarr-labs/homarr/commit/15197897ac3e2f3ed1dcc24c5f64748eaa52f98f))
|
||||
* **permissions:** unable to edit board with full board permissions ([#2625](https://github.com/homarr-labs/homarr/issues/2625)) ([4cc3739](https://github.com/homarr-labs/homarr/commit/4cc37397c2e56a97e6b1173735da78824c7b15e5))
|
||||
* prohibit dot in board name ([#2643](https://github.com/homarr-labs/homarr/issues/2643)) ([bb8cb6d](https://github.com/homarr-labs/homarr/commit/bb8cb6ddeda6906b3c43270ec9dd8ec66d27f0cd))
|
||||
|
||||
## [1.11.0](https://github.com/homarr-labs/homarr/compare/v1.10.0...v1.11.0) (2025-03-14)
|
||||
|
||||
### Features
|
||||
|
||||
+22
-22
@@ -48,21 +48,21 @@
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@homarr/widgets": "workspace:^0.1.0",
|
||||
"@mantine/colors-generator": "^7.17.2",
|
||||
"@mantine/core": "^7.17.2",
|
||||
"@mantine/dropzone": "^7.17.2",
|
||||
"@mantine/hooks": "^7.17.2",
|
||||
"@mantine/modals": "^7.17.2",
|
||||
"@mantine/tiptap": "^7.17.2",
|
||||
"@mantine/colors-generator": "^7.17.3",
|
||||
"@mantine/core": "^7.17.3",
|
||||
"@mantine/dropzone": "^7.17.3",
|
||||
"@mantine/hooks": "^7.17.3",
|
||||
"@mantine/modals": "^7.17.3",
|
||||
"@mantine/tiptap": "^7.17.3",
|
||||
"@million/lint": "1.0.14",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"@tanstack/react-query": "^5.68.0",
|
||||
"@tanstack/react-query-devtools": "^5.68.0",
|
||||
"@tanstack/react-query-next-experimental": "^5.68.0",
|
||||
"@trpc/client": "next",
|
||||
"@trpc/next": "next",
|
||||
"@trpc/react-query": "next",
|
||||
"@trpc/server": "next",
|
||||
"@tanstack/react-query": "^5.71.10",
|
||||
"@tanstack/react-query-devtools": "^5.71.10",
|
||||
"@tanstack/react-query-next-experimental": "^5.71.10",
|
||||
"@trpc/client": "^11.0.2",
|
||||
"@trpc/next": "^11.0.2",
|
||||
"@trpc/react-query": "^11.0.2",
|
||||
"@trpc/server": "^11.0.2",
|
||||
"@xterm/addon-canvas": "^0.7.0",
|
||||
"@xterm/addon-fit": "0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
@@ -74,16 +74,16 @@
|
||||
"glob": "^11.0.1",
|
||||
"jotai": "^2.12.2",
|
||||
"mantine-react-table": "2.0.0-beta.9",
|
||||
"next": "15.1.7",
|
||||
"next": "15.2.4",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"react-simple-code-editor": "^0.14.1",
|
||||
"sass": "^1.85.1",
|
||||
"sass": "^1.86.3",
|
||||
"superjson": "2.2.2",
|
||||
"swagger-ui-react": "^5.20.1",
|
||||
"swagger-ui-react": "^5.20.6",
|
||||
"use-deep-compare-effect": "^1.8.1",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
@@ -92,13 +92,13 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/chroma-js": "3.1.1",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/react": "19.1.0",
|
||||
"@types/react-dom": "19.1.1",
|
||||
"@types/swagger-ui-react": "^5.18.0",
|
||||
"concurrently": "^9.1.2",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"node-loader": "^2.1.0",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.2"
|
||||
|
||||
@@ -7,11 +7,11 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";
|
||||
import {
|
||||
createWSClient,
|
||||
httpBatchStreamLink,
|
||||
httpLink,
|
||||
isNonJsonSerializable,
|
||||
loggerLink,
|
||||
splitLink,
|
||||
unstable_httpBatchStreamLink,
|
||||
wsLink,
|
||||
} from "@trpc/client";
|
||||
import superjson from "superjson";
|
||||
@@ -90,7 +90,7 @@ export function TRPCReactProvider(props: PropsWithChildren) {
|
||||
url: getTrpcUrl(),
|
||||
headers: createHeadersCallbackForSource("nextjs-react (form-data)"),
|
||||
}),
|
||||
false: unstable_httpBatchStreamLink({
|
||||
false: httpBatchStreamLink({
|
||||
transformer: superjson,
|
||||
url: getTrpcUrl(),
|
||||
maxURLLength: 2083, // Suggested by tRPC: https://trpc.io/docs/client/links/httpBatchLink#setting-a-maximum-url-length
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { signIn } from "@homarr/auth/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
@@ -30,6 +31,13 @@ export const InitUserForm = () => {
|
||||
title: tUser("notification.success.title"),
|
||||
message: tUser("notification.success.message"),
|
||||
});
|
||||
|
||||
await signIn("credentials", {
|
||||
name: values.username,
|
||||
password: values.password,
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
await revalidatePathActionAsync("/init");
|
||||
},
|
||||
onError: (error) => {
|
||||
|
||||
@@ -66,15 +66,17 @@ export const JobsList = ({ initialJobs }: JobsListProps) => {
|
||||
{job.status && <TimeAgo timestamp={job.status.lastExecutionTimestamp} />}
|
||||
</Stack>
|
||||
|
||||
<ActionIcon
|
||||
onClick={() => handleJobTrigger(job)}
|
||||
disabled={job.status?.status === "running"}
|
||||
variant={"default"}
|
||||
size={"xl"}
|
||||
radius={"xl"}
|
||||
>
|
||||
<IconPlayerPlay stroke={1.5} />
|
||||
</ActionIcon>
|
||||
{!job.job.preventManualExecution && (
|
||||
<ActionIcon
|
||||
onClick={() => handleJobTrigger(job)}
|
||||
disabled={job.status?.status === "running"}
|
||||
variant={"default"}
|
||||
size={"xl"}
|
||||
radius={"xl"}
|
||||
>
|
||||
<IconPlayerPlay stroke={1.5} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
@@ -118,15 +118,13 @@ export default async function EditUserPage(props: Props) {
|
||||
<PingIconsEnabled user={user} />
|
||||
</Stack>
|
||||
|
||||
{isCredentialsUser && (
|
||||
<DangerZoneRoot>
|
||||
<DangerZoneItem
|
||||
label={t("user.action.delete.label")}
|
||||
description={t("user.action.delete.description")}
|
||||
action={<DeleteUserButton user={user} />}
|
||||
/>
|
||||
</DangerZoneRoot>
|
||||
)}
|
||||
<DangerZoneRoot>
|
||||
<DangerZoneItem
|
||||
label={t("user.action.delete.label")}
|
||||
description={t("user.action.delete.description")}
|
||||
action={<DeleteUserButton user={user} />}
|
||||
/>
|
||||
</DangerZoneRoot>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -26,6 +26,9 @@ const handlerAsync = async (req: NextRequest) => {
|
||||
endpoint: "/",
|
||||
router: appRouter,
|
||||
createContext: () => createTRPCContext({ session, headers: req.headers }),
|
||||
onError({ error, path, type }) {
|
||||
logger.error(new Error(`tRPC Error with ${type} on '${path}'`, { cause: error.cause }));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -31,9 +31,7 @@ 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}\n${error.stack}\n${error.cause}`,
|
||||
);
|
||||
logger.error(new Error(`tRPC Error with ${type} on '${path}'`, { cause: error.cause }));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export class DynamicSectionMockBuilder {
|
||||
id: createId(),
|
||||
kind: "dynamic",
|
||||
options: {
|
||||
title: "",
|
||||
borderColor: "",
|
||||
},
|
||||
layouts: [],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Card } from "@mantine/core";
|
||||
import { Badge, Box, Card } from "@mantine/core";
|
||||
|
||||
import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
|
||||
|
||||
@@ -17,7 +17,12 @@ export const BoardDynamicSection = ({ section }: Props) => {
|
||||
const options = section.options;
|
||||
|
||||
return (
|
||||
<Box className="grid-stack-item-content">
|
||||
<Box
|
||||
className="grid-stack-item-content"
|
||||
style={{
|
||||
overflow: "visible",
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
className={classes.itemCard}
|
||||
w="100%"
|
||||
@@ -25,14 +30,28 @@ export const BoardDynamicSection = ({ section }: Props) => {
|
||||
withBorder
|
||||
styles={{
|
||||
root: {
|
||||
overflow: "visible",
|
||||
"--opacity": board.opacity / 100,
|
||||
overflow: "hidden",
|
||||
"--border-color": options.borderColor !== "" ? options.borderColor : undefined,
|
||||
"--border-color": options.borderColor || undefined,
|
||||
},
|
||||
}}
|
||||
radius={board.itemRadius}
|
||||
p={0}
|
||||
>
|
||||
{options.title && (
|
||||
<Badge
|
||||
pos="absolute"
|
||||
top={-15}
|
||||
left={10}
|
||||
size="md"
|
||||
radius={board.itemRadius}
|
||||
color="var(--background-color)"
|
||||
c="var(--mantine-color-text)"
|
||||
bd="1px solid var(--border-color)"
|
||||
>
|
||||
{options.title}
|
||||
</Badge>
|
||||
)}
|
||||
{/* Use unique key by layout to reinitialize gridstack */}
|
||||
<GridStack key={`${currentLayoutId}-${section.id}`} section={section} className="min-row" />
|
||||
</Card>
|
||||
|
||||
@@ -17,6 +17,7 @@ export const addDynamicSectionCallback = () => (board: Board) => {
|
||||
id: createId(),
|
||||
kind: "dynamic",
|
||||
options: {
|
||||
title: "",
|
||||
borderColor: "",
|
||||
},
|
||||
layouts: createDynamicSectionLayouts(board, firstSection),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button, CloseButton, ColorInput, Group, Stack, useMantineTheme } from "@mantine/core";
|
||||
import { Button, CloseButton, ColorInput, Group, Stack, TextInput, useMantineTheme } from "@mantine/core";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { useZodForm } from "@homarr/form";
|
||||
@@ -30,6 +30,7 @@ export const DynamicSectionEditModal = createModal<ModalProps>(({ actions, inner
|
||||
})}
|
||||
>
|
||||
<Stack>
|
||||
<TextInput label={t("section.dynamic.option.title.label")} {...form.getInputProps("title")} />
|
||||
<ColorInput
|
||||
label={t("section.dynamic.option.borderColor.label")}
|
||||
format="hex"
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { Suspense, use } from "react";
|
||||
import { Indicator, Menu, Text } from "@mantine/core";
|
||||
import { IconBellRinging } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
interface UpdateIndicatorProps extends PropsWithChildren {
|
||||
availableUpdatesPromise: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]> | undefined;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export const UpdateIndicator = ({ children, availableUpdatesPromise, disabled }: UpdateIndicatorProps) => {
|
||||
if (disabled || availableUpdatesPromise === undefined) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={children}>
|
||||
<InnerUpdateIndicator availableUpdatesPromise={availableUpdatesPromise} disabled={disabled}>
|
||||
{children}
|
||||
</InnerUpdateIndicator>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
interface InnerUpdateIndicatorProps extends PropsWithChildren {
|
||||
availableUpdatesPromise: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]>;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const InnerUpdateIndicator = ({ children, disabled, availableUpdatesPromise }: InnerUpdateIndicatorProps) => {
|
||||
const availableUpdates = use(availableUpdatesPromise);
|
||||
|
||||
return (
|
||||
<Indicator
|
||||
disabled={!availableUpdates || availableUpdates.length === 0 || disabled}
|
||||
size={15}
|
||||
processing
|
||||
withBorder
|
||||
>
|
||||
{children}
|
||||
</Indicator>
|
||||
);
|
||||
};
|
||||
|
||||
interface AvailableUpdatesMenuItemProps {
|
||||
availableUpdatesPromise: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]> | undefined;
|
||||
}
|
||||
|
||||
export const AvailableUpdatesMenuItem = ({ availableUpdatesPromise }: AvailableUpdatesMenuItemProps) => {
|
||||
if (availableUpdatesPromise === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<InnerAvailableUpdatesMenuItem availableUpdatesPromise={availableUpdatesPromise} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
interface InnerAvailableUpdatesMenuItemProps {
|
||||
availableUpdatesPromise: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]>;
|
||||
}
|
||||
|
||||
const InnerAvailableUpdatesMenuItem = ({ availableUpdatesPromise }: InnerAvailableUpdatesMenuItemProps) => {
|
||||
const t = useScopedI18n("common.userAvatar.menu");
|
||||
const availableUpdates = use(availableUpdatesPromise);
|
||||
if (availableUpdates === undefined || availableUpdates.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const latestUpdate = availableUpdates.at(0);
|
||||
if (!latestUpdate) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.Item component={"a"} href={latestUpdate.url} target="_blank" leftSection={<IconBellRinging size="1rem" />}>
|
||||
<Text fw="bold" size="sm">
|
||||
{t("updateAvailable", {
|
||||
countUpdates: String(availableUpdates.length),
|
||||
tag: latestUpdate.tagName,
|
||||
})}
|
||||
</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,21 +1,25 @@
|
||||
import { Indicator, UnstyledButton } from "@mantine/core";
|
||||
import { Suspense } from "react";
|
||||
import { UnstyledButton } from "@mantine/core";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
|
||||
import { CurrentUserAvatar } from "~/components/user-avatar";
|
||||
import { UserAvatarMenu } from "~/components/user-avatar-menu";
|
||||
import { UpdateIndicator } from "./update";
|
||||
|
||||
export const UserButton = async () => {
|
||||
const session = await auth();
|
||||
const isAdmin = session?.user.permissions.includes("admin");
|
||||
const data = isAdmin ? await api.updateChecker.getAvailableUpdates() : undefined;
|
||||
const availableUpdatesPromise = isAdmin ? api.updateChecker.getAvailableUpdates() : undefined;
|
||||
return (
|
||||
<UserAvatarMenu availableUpdates={data}>
|
||||
<UserAvatarMenu availableUpdatesPromise={availableUpdatesPromise}>
|
||||
<UnstyledButton>
|
||||
<Indicator disabled={data?.length === 0 || !isAdmin} size={15} processing withBorder>
|
||||
<CurrentUserAvatar size="md" />
|
||||
</Indicator>
|
||||
<Suspense fallback={<CurrentUserAvatar size="md" />}>
|
||||
<UpdateIndicator availableUpdatesPromise={availableUpdatesPromise} disabled={!isAdmin}>
|
||||
<CurrentUserAvatar size="md" />
|
||||
</UpdateIndicator>
|
||||
</Suspense>
|
||||
</UnstyledButton>
|
||||
</UserAvatarMenu>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useRouter } from "next/navigation";
|
||||
import { Center, Menu, Stack, Text, useMantineColorScheme } from "@mantine/core";
|
||||
import { useHotkeys, useTimeout } from "@mantine/hooks";
|
||||
import {
|
||||
IconBellRinging,
|
||||
IconCheck,
|
||||
IconHome,
|
||||
IconLogin,
|
||||
@@ -25,13 +24,14 @@ import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { useAuthContext } from "~/app/[locale]/_client-providers/session";
|
||||
import { CurrentLanguageCombobox } from "./language/current-language-combobox";
|
||||
import { AvailableUpdatesMenuItem } from "./layout/header/update";
|
||||
|
||||
interface UserAvatarMenuProps {
|
||||
children: ReactNode;
|
||||
availableUpdates?: RouterOutputs["updateChecker"]["getAvailableUpdates"];
|
||||
availableUpdatesPromise?: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]>;
|
||||
}
|
||||
|
||||
export const UserAvatarMenu = ({ children, availableUpdates }: UserAvatarMenuProps) => {
|
||||
export const UserAvatarMenu = ({ children, availableUpdatesPromise }: UserAvatarMenuProps) => {
|
||||
const t = useScopedI18n("common.userAvatar.menu");
|
||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||
useHotkeys([["mod+J", toggleColorScheme]]);
|
||||
@@ -65,24 +65,7 @@ export const UserAvatarMenu = ({ children, availableUpdates }: UserAvatarMenuPro
|
||||
// We use keepMounted so we can add event listeners to prevent navigating away without saving the board
|
||||
<Menu width={300} withArrow withinPortal keepMounted>
|
||||
<Menu.Dropdown>
|
||||
{availableUpdates && availableUpdates.length > 0 && availableUpdates[0] && (
|
||||
<>
|
||||
<Menu.Item
|
||||
component={"a"}
|
||||
href={availableUpdates[0].url}
|
||||
target="_blank"
|
||||
leftSection={<IconBellRinging size="1rem" />}
|
||||
>
|
||||
<Text fw="bold" size="sm">
|
||||
{t("updateAvailable", {
|
||||
countUpdates: String(availableUpdates.length),
|
||||
tag: availableUpdates[0].tagName,
|
||||
})}
|
||||
</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
</>
|
||||
)}
|
||||
<AvailableUpdatesMenuItem availableUpdatesPromise={availableUpdatesPromise} />
|
||||
<Menu.Item onClick={toggleColorScheme} leftSection={<ColorSchemeIcon size="1rem" />}>
|
||||
{colorSchemeText}
|
||||
</Menu.Item>
|
||||
|
||||
@@ -38,15 +38,15 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^16.4.7",
|
||||
"superjson": "2.2.2",
|
||||
"undici": "7.5.0"
|
||||
"undici": "7.7.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.13.10",
|
||||
"@types/node": "^22.14.0",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"prettier": "^3.5.3",
|
||||
"tsx": "4.19.3",
|
||||
"typescript": "^5.8.2"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// This import has to be the first import in the file so that the agent is overridden before any other modules are imported.
|
||||
import "./undici-log-agent-override";
|
||||
|
||||
import { registerCronJobRunner } from "@homarr/cron-job-runner";
|
||||
import { registerCronJobRunner } from "@homarr/cron-job-runner/register";
|
||||
import { jobGroup } from "@homarr/cron-jobs";
|
||||
|
||||
void (async () => {
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/ws": "^8.18.0",
|
||||
"eslint": "^9.22.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"eslint": "^9.23.0",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
|
||||
+10
-10
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homarr",
|
||||
"version": "1.11.0",
|
||||
"version": "1.14.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "cross-env CI=true turbo build",
|
||||
@@ -38,22 +38,22 @@
|
||||
"@semantic-release/github": "^11.0.1",
|
||||
"@semantic-release/npm": "^12.0.1",
|
||||
"@semantic-release/release-notes-generator": "^14.0.3",
|
||||
"@turbo/gen": "^2.4.4",
|
||||
"@turbo/gen": "^2.5.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitest/coverage-v8": "^3.0.8",
|
||||
"@vitest/ui": "^3.0.8",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"@vitest/ui": "^3.1.1",
|
||||
"conventional-changelog-conventionalcommits": "^8.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"jsdom": "^26.0.0",
|
||||
"prettier": "^3.5.3",
|
||||
"semantic-release": "^24.2.3",
|
||||
"testcontainers": "^10.20.0",
|
||||
"turbo": "^2.4.4",
|
||||
"testcontainers": "^10.24.0",
|
||||
"turbo": "^2.5.0",
|
||||
"typescript": "^5.8.2",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.0.8"
|
||||
"vitest": "^3.1.1"
|
||||
},
|
||||
"packageManager": "pnpm@10.6.3",
|
||||
"packageManager": "pnpm@10.7.1",
|
||||
"engines": {
|
||||
"node": ">=22.14.0"
|
||||
},
|
||||
@@ -69,13 +69,13 @@
|
||||
"tree-sitter",
|
||||
"tree-sitter-json"
|
||||
],
|
||||
"allowNonAppliedPatches": true,
|
||||
"overrides": {
|
||||
"proxmox-api>undici": "7.5.0"
|
||||
"proxmox-api>undici": "7.7.0"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"pretty-print-error": "patches/pretty-print-error.patch"
|
||||
},
|
||||
"allowUnusedPatches": true,
|
||||
"ignoredBuiltDependencies": [
|
||||
"@scarf/scarf",
|
||||
"core-js-pure",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -40,24 +40,24 @@
|
||||
"@homarr/request-handler": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@kubernetes/client-node": "^1.1.0",
|
||||
"@trpc/client": "next",
|
||||
"@trpc/react-query": "next",
|
||||
"@trpc/server": "next",
|
||||
"@kubernetes/client-node": "^1.1.1",
|
||||
"@trpc/client": "^11.0.2",
|
||||
"@trpc/react-query": "^11.0.2",
|
||||
"@trpc/server": "^11.0.2",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"next": "15.1.7",
|
||||
"next": "15.2.4",
|
||||
"pretty-print-error": "^1.1.2",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"superjson": "2.2.2",
|
||||
"trpc-to-openapi": "^2.1.3",
|
||||
"trpc-to-openapi": "^2.1.5",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
|
||||
@@ -251,6 +251,13 @@ export const boardRouter = createTRPCRouter({
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const boardId = createId();
|
||||
|
||||
const user = await ctx.db.query.users.findFirst({
|
||||
where: eq(users.id, ctx.session.user.id),
|
||||
columns: {
|
||||
homeBoardId: true,
|
||||
},
|
||||
});
|
||||
|
||||
const createBoardCollection = createDbInsertCollectionWithoutTransaction(["boards", "sections", "layouts"]);
|
||||
|
||||
createBoardCollection.boards.push({
|
||||
@@ -275,6 +282,12 @@ export const boardRouter = createTRPCRouter({
|
||||
});
|
||||
|
||||
await createBoardCollection.insertAllAsync(ctx.db);
|
||||
|
||||
if (!user?.homeBoardId) {
|
||||
await ctx.db.update(users).set({ homeBoardId: boardId }).where(eq(users.id, ctx.session.user.id));
|
||||
}
|
||||
|
||||
return { boardId };
|
||||
}),
|
||||
duplicateBoard: permissionRequiredProcedure
|
||||
.requiresPermission("board-create")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { observable } from "@trpc/server/observable";
|
||||
|
||||
import { jobNameSchema, triggerCronJobAsync } from "@homarr/cron-job-runner";
|
||||
import { objectEntries } from "@homarr/common";
|
||||
import { cronJobNames, cronJobs, jobNameSchema, triggerCronJobAsync } from "@homarr/cron-job-runner";
|
||||
import type { TaskStatus } from "@homarr/cron-job-status";
|
||||
import { createCronJobStatusChannel } from "@homarr/cron-job-status";
|
||||
import { jobGroup } from "@homarr/cron-jobs";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
|
||||
@@ -16,18 +16,17 @@ export const cronJobsRouter = createTRPCRouter({
|
||||
await triggerCronJobAsync(input);
|
||||
}),
|
||||
getJobs: permissionRequiredProcedure.requiresPermission("admin").query(() => {
|
||||
const registry = jobGroup.getJobRegistry();
|
||||
return [...registry.values()].map((job) => ({
|
||||
name: job.name,
|
||||
expression: job.cronExpression,
|
||||
return objectEntries(cronJobs).map(([name, options]) => ({
|
||||
name,
|
||||
preventManualExecution: options.preventManualExecution,
|
||||
}));
|
||||
}),
|
||||
subscribeToStatusUpdates: permissionRequiredProcedure.requiresPermission("admin").subscription(() => {
|
||||
return observable<TaskStatus>((emit) => {
|
||||
const unsubscribes: (() => void)[] = [];
|
||||
|
||||
for (const job of jobGroup.getJobRegistry().values()) {
|
||||
const channel = createCronJobStatusChannel(job.name);
|
||||
for (const name of cronJobNames) {
|
||||
const channel = createCronJobStatusChannel(name);
|
||||
const unsubscribe = channel.subscribe((data) => {
|
||||
emit.next(data);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { formatError } from "pretty-print-error";
|
||||
|
||||
import { decryptSecret } from "@homarr/common/server";
|
||||
import type { Integration } from "@homarr/db/schema";
|
||||
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
||||
@@ -41,7 +39,10 @@ export const testConnectionAsync = async (
|
||||
};
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`Failed to decrypt secret from database integration="${integration.name}" secretKind="${secret.kind}"\n${formatError(error)}`,
|
||||
new Error(
|
||||
`Failed to decrypt secret from database integration="${integration.name}" secretKind="${secret.kind}"`,
|
||||
{ cause: error },
|
||||
),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -65,9 +65,7 @@ export class KubernetesClient {
|
||||
}
|
||||
|
||||
public static getInstance(): KubernetesClient {
|
||||
if (!KubernetesClient.instance) {
|
||||
KubernetesClient.instance = new KubernetesClient();
|
||||
}
|
||||
KubernetesClient.instance ??= new KubernetesClient();
|
||||
return KubernetesClient.instance;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import { formatError } from "pretty-print-error";
|
||||
|
||||
import { logger } from "@homarr/log";
|
||||
import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
|
||||
|
||||
@@ -12,7 +10,7 @@ export const updateCheckerRouter = createTRPCRouter({
|
||||
const data = await handler.getCachedOrUpdatedDataAsync({});
|
||||
return data.data.availableUpdates;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get available updates\n${formatError(error)}`);
|
||||
logger.error(new Error("Failed to get available updates", { cause: error }));
|
||||
return undefined; // We return undefined to not show the indicator in the UI
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -13,6 +13,7 @@ import { notebookRouter } from "./notebook";
|
||||
import { optionsRouter } from "./options";
|
||||
import { rssFeedRouter } from "./rssFeed";
|
||||
import { smartHomeRouter } from "./smart-home";
|
||||
import { stockPriceRouter } from "./stocks";
|
||||
import { weatherRouter } from "./weather";
|
||||
|
||||
export const widgetRouter = createTRPCRouter({
|
||||
@@ -21,6 +22,7 @@ export const widgetRouter = createTRPCRouter({
|
||||
app: appRouter,
|
||||
dnsHole: dnsHoleRouter,
|
||||
smartHome: smartHomeRouter,
|
||||
stockPrice: stockPriceRouter,
|
||||
mediaServer: mediaServerRouter,
|
||||
calendar: calendarRouter,
|
||||
downloads: downloadsRouter,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { fetchStockPriceHandler } from "@homarr/request-handler/stock-price";
|
||||
|
||||
import { stockPriceTimeFrames } from "../../../../widgets/src/stocks";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
const stockPriceInputSchema = z.object({
|
||||
stock: z.string().nonempty(),
|
||||
timeRange: z.enum(stockPriceTimeFrames.range),
|
||||
timeInterval: z.enum(stockPriceTimeFrames.interval),
|
||||
});
|
||||
|
||||
export const stockPriceRouter = createTRPCRouter({
|
||||
getPriceHistory: publicProcedure.input(stockPriceInputSchema).query(async ({ input }) => {
|
||||
const innerHandler = fetchStockPriceHandler.handler({
|
||||
stock: input.stock,
|
||||
timeRange: input.timeRange,
|
||||
timeInterval: input.timeInterval,
|
||||
});
|
||||
return await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||
}),
|
||||
});
|
||||
@@ -2,7 +2,6 @@ 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";
|
||||
@@ -36,8 +35,7 @@ export const createConfiguration = (
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error(formatError(error));
|
||||
logger.error(formatError(error.cause));
|
||||
logger.error(error);
|
||||
},
|
||||
},
|
||||
trustHost: true,
|
||||
|
||||
@@ -39,6 +39,7 @@ export const env = createEnv({
|
||||
AUTH_OIDC_SCOPE_OVERWRITE: z.string().min(1).default("openid email profile groups"),
|
||||
AUTH_OIDC_GROUPS_ATTRIBUTE: z.string().default("groups"), // Is used in the signIn event to assign the correct groups, key is from object of decoded id_token
|
||||
AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE: z.string().optional(),
|
||||
AUTH_OIDC_FORCE_USERINFO: createBooleanSchema(false),
|
||||
}
|
||||
: {}),
|
||||
...(authProviders.includes("ldap")
|
||||
|
||||
@@ -34,12 +34,12 @@
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cookies": "^0.9.1",
|
||||
"ldapts": "7.3.1",
|
||||
"next": "15.1.7",
|
||||
"ldapts": "7.3.3",
|
||||
"next": "15.2.4",
|
||||
"next-auth": "5.0.0-beta.25",
|
||||
"pretty-print-error": "^1.1.2",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -48,7 +48,7 @@
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/bcrypt": "5.0.2",
|
||||
"@types/cookies": "0.9.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
|
||||
@@ -25,11 +25,15 @@ export const constructBoardPermissions = (board: BoardPermissionsProps, session:
|
||||
const creatorId = "creator" in board ? board.creator?.id : board.creatorId;
|
||||
|
||||
return {
|
||||
hasFullAccess: session?.user.id === creatorId || (session?.user.permissions.includes("board-full-all") ?? false),
|
||||
hasFullAccess:
|
||||
session?.user.id === creatorId ||
|
||||
board.userPermissions.some(({ permission }) => permission === "full") ||
|
||||
board.groupPermissions.some(({ permission }) => permission === "full") ||
|
||||
(session?.user.permissions.includes("board-full-all") ?? false),
|
||||
hasChangeAccess:
|
||||
session?.user.id === creatorId ||
|
||||
board.userPermissions.some(({ permission }) => permission === "modify") ||
|
||||
board.groupPermissions.some(({ permission }) => permission === "modify") ||
|
||||
board.userPermissions.some(({ permission }) => permission === "modify" || permission === "full") ||
|
||||
board.groupPermissions.some(({ permission }) => permission === "modify" || permission === "full") ||
|
||||
(session?.user.permissions.includes("board-modify-all") ?? false) ||
|
||||
(session?.user.permissions.includes("board-full-all") ?? false),
|
||||
hasViewAccess:
|
||||
|
||||
@@ -22,6 +22,31 @@ export const OidcProvider = (headers: ReadonlyHeaders | null): OIDCConfig<Profil
|
||||
redirect_uri: createRedirectUri(headers, "/api/auth/callback/oidc", "https"),
|
||||
},
|
||||
},
|
||||
token: {
|
||||
// Providers like fusionauth may return www-authenticate which results in an error
|
||||
// https://github.com/nextauthjs/next-auth/issues/8745
|
||||
// https://github.com/homarr-labs/homarr/issues/2690
|
||||
conform: (response: Response) => {
|
||||
if (response.status === 401) return response;
|
||||
|
||||
const newHeaders = Array.from(response.headers.entries())
|
||||
.filter(([key]) => key.toLowerCase() !== "www-authenticate")
|
||||
.reduce((headers, [key, value]) => {
|
||||
headers.append(key, value);
|
||||
return headers;
|
||||
}, new Headers());
|
||||
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: newHeaders,
|
||||
});
|
||||
},
|
||||
},
|
||||
// idToken false forces the use of the userinfo endpoint
|
||||
// Userinfo endpoint is required for authelia since v4.39
|
||||
// See https://github.com/homarr-labs/homarr/issues/2635
|
||||
idToken: !env.AUTH_OIDC_FORCE_USERINFO,
|
||||
profile(profile) {
|
||||
if (!profile.sub) {
|
||||
throw new Error(`OIDC provider did not return a sub property='${Object.keys(profile).join(",")}'`);
|
||||
|
||||
@@ -25,14 +25,14 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/api": "workspace:^0.1.0",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0"
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -23,13 +23,13 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"undici": "7.5.0"
|
||||
"undici": "7.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { Agent } from "node:https";
|
||||
import { Agent as HttpsAgent } from "node:https";
|
||||
import path from "node:path";
|
||||
import { rootCertificates } from "node:tls";
|
||||
import axios from "axios";
|
||||
@@ -70,12 +70,16 @@ export const createCertificateAgentAsync = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const createAxiosCertificateInstanceAsync = async () => {
|
||||
export const createHttpsAgentAsync = async () => {
|
||||
const customCertificates = await loadCustomRootCertificatesAsync();
|
||||
return new HttpsAgent({
|
||||
ca: rootCertificates.concat(customCertificates.map((cert) => cert.content)),
|
||||
});
|
||||
};
|
||||
|
||||
export const createAxiosCertificateInstanceAsync = async () => {
|
||||
return axios.create({
|
||||
httpsAgent: new Agent({
|
||||
ca: rootCertificates.concat(customCertificates.map((cert) => cert.content)),
|
||||
}),
|
||||
httpsAgent: await createHttpsAgentAsync(),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -27,13 +27,14 @@
|
||||
"@homarr/auth": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"dotenv": "^16.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export const fixUsernames = command({
|
||||
|
||||
for (const user of credentialUsers) {
|
||||
if (!user.name) continue;
|
||||
if (user.name !== user.name.toLowerCase()) continue;
|
||||
if (user.name === user.name.toLowerCase()) continue;
|
||||
|
||||
await db
|
||||
.update(users)
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { command, string } from "@drizzle-team/brocli";
|
||||
|
||||
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
|
||||
import { generateSecureRandomToken } from "@homarr/common/server";
|
||||
import { and, count, createId, db, eq } from "@homarr/db";
|
||||
import { getMaxGroupPositionAsync } from "@homarr/db/queries";
|
||||
import { groupMembers, groupPermissions, groups, users } from "@homarr/db/schema";
|
||||
import { usernameSchema } from "@homarr/validation";
|
||||
|
||||
export const recreateAdmin = command({
|
||||
name: "recreate-admin",
|
||||
desc: "Recreate credentials admin user if none exists anymore",
|
||||
options: {
|
||||
username: string("username").required().alias("u").desc("Name of the admin"),
|
||||
},
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
handler: async (options) => {
|
||||
if (!process.env.AUTH_PROVIDERS?.toLowerCase().includes("credentials")) {
|
||||
console.error("Credentials provider is not enabled");
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await usernameSchema.safeParseAsync(options.username);
|
||||
|
||||
if (!result.success) {
|
||||
console.error("Invalid username:");
|
||||
console.error(result.error.errors.map((error) => `- ${error.message}`).join("\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
const totalCount = await db
|
||||
.select({
|
||||
count: count(),
|
||||
})
|
||||
.from(groupPermissions)
|
||||
.leftJoin(groupMembers, eq(groupMembers.groupId, groupPermissions.groupId))
|
||||
.leftJoin(users, eq(users.id, groupMembers.userId))
|
||||
.where(and(eq(groupPermissions.permission, "admin"), eq(users.provider, "credentials")))
|
||||
.then((rows) => rows.at(0)?.count ?? 0);
|
||||
|
||||
if (totalCount > 0) {
|
||||
console.error("Credentials admin user exists");
|
||||
return;
|
||||
}
|
||||
|
||||
const existingUser = await db.query.users.findFirst({
|
||||
where: eq(users.name, result.data),
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
console.error("User with this name already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
const temporaryGroupId = createId();
|
||||
|
||||
const maxPosition = await getMaxGroupPositionAsync(db);
|
||||
await db.insert(groups).values({
|
||||
id: temporaryGroupId,
|
||||
name: temporaryGroupId,
|
||||
position: maxPosition + 1,
|
||||
});
|
||||
|
||||
await db.insert(groupPermissions).values({
|
||||
groupId: temporaryGroupId,
|
||||
permission: "admin",
|
||||
});
|
||||
|
||||
const salt = await createSaltAsync();
|
||||
const password = generateSecureRandomToken(24);
|
||||
const hashedPassword = await hashPasswordAsync(password, salt);
|
||||
|
||||
const userId = createId();
|
||||
await db.insert(users).values({
|
||||
id: userId,
|
||||
name: result.data,
|
||||
provider: "credentials",
|
||||
password: hashedPassword,
|
||||
salt,
|
||||
});
|
||||
|
||||
await db.insert(groupMembers).values({
|
||||
groupId: temporaryGroupId,
|
||||
userId,
|
||||
});
|
||||
|
||||
console.log(
|
||||
"We created a new admin user for you. Please keep in mind, that the admin group of it has a temporary name. You should change it to something more meaningful.",
|
||||
);
|
||||
console.log(`\tUsername: ${result.data}`);
|
||||
console.log(`\tPassword: ${password}`);
|
||||
console.log(`\tGroup: ${temporaryGroupId}`);
|
||||
console.log(""); // Empty line for better readability
|
||||
},
|
||||
});
|
||||
@@ -1,9 +1,10 @@
|
||||
import { run } from "@drizzle-team/brocli";
|
||||
|
||||
import { fixUsernames } from "./commands/fix-usernames";
|
||||
import { recreateAdmin } from "./commands/recreate-admin";
|
||||
import { resetPassword } from "./commands/reset-password";
|
||||
|
||||
const commands = [resetPassword, fixUsernames];
|
||||
const commands = [resetPassword, fixUsernames, recreateAdmin];
|
||||
|
||||
void run(commands, {
|
||||
name: "homarr-cli",
|
||||
|
||||
@@ -30,17 +30,17 @@
|
||||
"@homarr/env": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"next": "15.1.7",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"undici": "7.5.0",
|
||||
"next": "15.2.4",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"undici": "7.7.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,9 @@ export const extractBaseUrlFromHeaders = (
|
||||
headers: ReadonlyHeaders,
|
||||
fallbackProtocol: "http" | "https" = "http",
|
||||
): `${string}://${string}` => {
|
||||
let protocol = headers.get("x-forwarded-proto");
|
||||
|
||||
// If the protocol is not set or an empty string
|
||||
if (!protocol) {
|
||||
protocol = fallbackProtocol;
|
||||
}
|
||||
// For empty string we also use the fallback protocol
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
let protocol = headers.get("x-forwarded-proto") || fallbackProtocol;
|
||||
|
||||
// @see https://support.glitch.com/t/x-forwarded-proto-contains-multiple-protocols/17219
|
||||
if (protocol.includes(",")) {
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./index.ts"
|
||||
".": "./index.ts",
|
||||
"./register": "./src/register.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
@@ -22,15 +23,17 @@
|
||||
},
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"@homarr/redis": "workspace:^0.1.0"
|
||||
"@homarr/redis": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,29 @@
|
||||
import { objectKeys } from "@homarr/common";
|
||||
import type { JobGroupKeys } from "@homarr/cron-jobs";
|
||||
import { jobGroup } from "@homarr/cron-jobs";
|
||||
import { createSubPubChannel } from "@homarr/redis";
|
||||
import { zodEnumFromArray } from "@homarr/validation";
|
||||
|
||||
import { createSubPubChannel } from "../../redis/src/lib/channel";
|
||||
import { zodEnumFromArray } from "../../validation/src/enums";
|
||||
export const cronJobRunnerChannel = createSubPubChannel<JobGroupKeys>("cron-job-runner", { persist: false });
|
||||
|
||||
const cronJobRunnerChannel = createSubPubChannel<JobGroupKeys>("cron-job-runner", { persist: false });
|
||||
|
||||
/**
|
||||
* Registers the cron job runner to listen to the Redis PubSub channel.
|
||||
*/
|
||||
export const registerCronJobRunner = () => {
|
||||
cronJobRunnerChannel.subscribe((jobName) => {
|
||||
jobGroup.runManually(jobName);
|
||||
});
|
||||
};
|
||||
export const cronJobs = {
|
||||
analytics: { preventManualExecution: true },
|
||||
iconsUpdater: { preventManualExecution: false },
|
||||
ping: { preventManualExecution: false },
|
||||
smartHomeEntityState: { preventManualExecution: false },
|
||||
mediaServer: { preventManualExecution: false },
|
||||
mediaOrganizer: { preventManualExecution: false },
|
||||
downloads: { preventManualExecution: false },
|
||||
dnsHole: { preventManualExecution: false },
|
||||
mediaRequestStats: { preventManualExecution: false },
|
||||
mediaRequestList: { preventManualExecution: false },
|
||||
rssFeeds: { preventManualExecution: false },
|
||||
indexerManager: { preventManualExecution: false },
|
||||
healthMonitoring: { preventManualExecution: false },
|
||||
sessionCleanup: { preventManualExecution: false },
|
||||
updateChecker: { preventManualExecution: false },
|
||||
mediaTranscoding: { preventManualExecution: false },
|
||||
minecraftServerStatus: { preventManualExecution: false },
|
||||
} satisfies Record<JobGroupKeys, { preventManualExecution?: boolean }>;
|
||||
|
||||
/**
|
||||
* Triggers a cron job to run immediately.
|
||||
@@ -21,7 +31,12 @@ export const registerCronJobRunner = () => {
|
||||
* @param jobName name of the job to be triggered
|
||||
*/
|
||||
export const triggerCronJobAsync = async (jobName: JobGroupKeys) => {
|
||||
if (cronJobs[jobName].preventManualExecution) {
|
||||
throw new Error(`The job "${jobName}" can not be executed manually`);
|
||||
}
|
||||
await cronJobRunnerChannel.publishAsync(jobName);
|
||||
};
|
||||
|
||||
export const jobNameSchema = zodEnumFromArray(jobGroup.getKeys());
|
||||
export const cronJobNames = objectKeys(cronJobs);
|
||||
|
||||
export const jobNameSchema = zodEnumFromArray(cronJobNames);
|
||||
@@ -0,0 +1,12 @@
|
||||
import { jobGroup } from "@homarr/cron-jobs";
|
||||
|
||||
import { cronJobRunnerChannel } from ".";
|
||||
|
||||
/**
|
||||
* Registers the cron job runner to listen to the Redis PubSub channel.
|
||||
*/
|
||||
export const registerCronJobRunner = () => {
|
||||
cronJobRunnerChannel.subscribe((jobName) => {
|
||||
jobGroup.runManually(jobName);
|
||||
});
|
||||
};
|
||||
@@ -29,7 +29,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,22 @@
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
import { objectKeys } from "@homarr/common";
|
||||
import { everyoneGroup } from "@homarr/definitions";
|
||||
import { createDocumentationLink, everyoneGroup } from "@homarr/definitions";
|
||||
import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server-settings";
|
||||
|
||||
import { createId, eq } from "..";
|
||||
import type { Database } from "..";
|
||||
import { onboarding, serverSettings } from "../schema";
|
||||
import { createId, eq } from "..";
|
||||
import {
|
||||
getServerSettingByKeyAsync,
|
||||
insertServerSettingByKeyAsync,
|
||||
updateServerSettingByKeyAsync,
|
||||
} from "../queries/server-setting";
|
||||
import { onboarding, searchEngines } from "../schema";
|
||||
import { groups } from "../schema/mysql";
|
||||
|
||||
export const seedDataAsync = async (db: Database) => {
|
||||
await seedEveryoneGroupAsync(db);
|
||||
await seedOnboardingAsync(db);
|
||||
await seedServerSettingsAsync(db);
|
||||
await seedDefaultSearchEnginesAsync(db);
|
||||
};
|
||||
|
||||
const seedEveryoneGroupAsync = async (db: Database) => {
|
||||
@@ -48,21 +52,73 @@ const seedOnboardingAsync = async (db: Database) => {
|
||||
console.log("Created onboarding step through seed");
|
||||
};
|
||||
|
||||
const seedDefaultSearchEnginesAsync = async (db: Database) => {
|
||||
const existingSearchEngines = await db.$count(searchEngines);
|
||||
|
||||
if (existingSearchEngines > 0) {
|
||||
console.log("Skipping seeding of default search engines as some already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
const homarrId = createId();
|
||||
const defaultSearchEngines = [
|
||||
{
|
||||
id: createId(),
|
||||
name: "Google",
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/google.svg",
|
||||
short: "g",
|
||||
description: "Search the web with Google",
|
||||
urlTemplate: "https://www.google.com/search?q=%s",
|
||||
type: "generic" as const,
|
||||
integrationId: null,
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
name: "YouTube",
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/youtube.svg",
|
||||
short: "yt",
|
||||
description: "Search for videos on YouTube",
|
||||
urlTemplate: "https://www.youtube.com/results?search_query=%s",
|
||||
type: "generic" as const,
|
||||
integrationId: null,
|
||||
},
|
||||
{
|
||||
id: homarrId,
|
||||
name: "Homarr Docs",
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/homarr.svg",
|
||||
short: "docs",
|
||||
description: "Search the Homarr documentation",
|
||||
urlTemplate: createDocumentationLink("/search", undefined, { q: "%s" }),
|
||||
type: "generic" as const,
|
||||
integrationId: null,
|
||||
},
|
||||
];
|
||||
|
||||
await db.insert(searchEngines).values(defaultSearchEngines);
|
||||
console.log(`Created ${defaultSearchEngines.length} default search engines through seeding process`);
|
||||
|
||||
// Set Homarr docs as the default search engine in server settings
|
||||
const searchSettings = await getServerSettingByKeyAsync(db, "search");
|
||||
|
||||
await updateServerSettingByKeyAsync(db, "search", {
|
||||
...searchSettings,
|
||||
defaultSearchEngineId: homarrId,
|
||||
});
|
||||
console.log("Set Homarr docs as the default search engine");
|
||||
};
|
||||
|
||||
const seedServerSettingsAsync = async (db: Database) => {
|
||||
const serverSettingsData = await db.query.serverSettings.findMany();
|
||||
|
||||
for (const settingsKey of defaultServerSettingsKeys) {
|
||||
const currentDbEntry = serverSettingsData.find((setting) => setting.settingKey === settingsKey);
|
||||
if (!currentDbEntry) {
|
||||
await db.insert(serverSettings).values({
|
||||
settingKey: settingsKey,
|
||||
value: SuperJSON.stringify(defaultServerSettings[settingsKey]),
|
||||
});
|
||||
await insertServerSettingByKeyAsync(db, settingsKey, defaultServerSettings[settingsKey]);
|
||||
console.log(`Created serverSetting through seed key=${settingsKey}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentSettings = SuperJSON.parse<Record<string, unknown>>(currentDbEntry.value);
|
||||
const currentSettings = await getServerSettingByKeyAsync(db, settingsKey);
|
||||
const defaultSettings = defaultServerSettings[settingsKey];
|
||||
const missingKeys = objectKeys(defaultSettings).filter((key) => !(key in currentSettings));
|
||||
|
||||
@@ -71,12 +127,7 @@ const seedServerSettingsAsync = async (db: Database) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
await db
|
||||
.update(serverSettings)
|
||||
.set({
|
||||
value: SuperJSON.stringify({ ...defaultSettings, ...currentSettings }), // Add missing keys
|
||||
})
|
||||
.where(eq(serverSettings.settingKey, settingsKey));
|
||||
await updateServerSettingByKeyAsync(db, settingsKey, { ...defaultSettings, ...currentSettings });
|
||||
console.log(`Updated serverSetting through seed key=${settingsKey}`);
|
||||
}
|
||||
};
|
||||
@@ -44,23 +44,23 @@
|
||||
"@homarr/env": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.17.2",
|
||||
"@mantine/core": "^7.17.3",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@testcontainers/mysql": "^10.20.0",
|
||||
"better-sqlite3": "^11.9.0",
|
||||
"@testcontainers/mysql": "^10.24.0",
|
||||
"better-sqlite3": "^11.9.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"drizzle-kit": "^0.30.5",
|
||||
"drizzle-orm": "^0.40.0",
|
||||
"drizzle-zod": "^0.7.0",
|
||||
"mysql2": "3.13.0"
|
||||
"drizzle-kit": "^0.30.6",
|
||||
"drizzle-orm": "^0.41.0",
|
||||
"drizzle-zod": "^0.7.1",
|
||||
"mysql2": "3.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/better-sqlite3": "7.6.12",
|
||||
"@types/better-sqlite3": "7.6.13",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"prettier": "^3.5.3",
|
||||
"tsx": "4.19.3",
|
||||
"typescript": "^5.8.2"
|
||||
|
||||
@@ -50,3 +50,14 @@ export const updateServerSettingByKeyAsync = async <TKey extends keyof ServerSet
|
||||
})
|
||||
.where(eq(serverSettings.settingKey, key));
|
||||
};
|
||||
|
||||
export const insertServerSettingByKeyAsync = async <TKey extends keyof ServerSettings>(
|
||||
db: Database,
|
||||
key: TKey,
|
||||
value: ServerSettings[TKey],
|
||||
) => {
|
||||
await db.insert(serverSettings).values({
|
||||
settingKey: key,
|
||||
value: SuperJSON.stringify(value),
|
||||
});
|
||||
};
|
||||
@@ -29,7 +29,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,525 @@ export type HomarrDocumentationPath =
|
||||
| "/blog/tags/version"
|
||||
| "/blog/translations"
|
||||
| "/search"
|
||||
| "/docs/1.10.0/tags"
|
||||
| "/docs/1.10.0/tags/active-directory"
|
||||
| "/docs/1.10.0/tags/ad-guard"
|
||||
| "/docs/1.10.0/tags/ad-guard-home"
|
||||
| "/docs/1.10.0/tags/administration"
|
||||
| "/docs/1.10.0/tags/advanced"
|
||||
| "/docs/1.10.0/tags/analytics"
|
||||
| "/docs/1.10.0/tags/api"
|
||||
| "/docs/1.10.0/tags/apps"
|
||||
| "/docs/1.10.0/tags/banner"
|
||||
| "/docs/1.10.0/tags/blocking"
|
||||
| "/docs/1.10.0/tags/boards"
|
||||
| "/docs/1.10.0/tags/bookmark"
|
||||
| "/docs/1.10.0/tags/bookmarks"
|
||||
| "/docs/1.10.0/tags/caddy"
|
||||
| "/docs/1.10.0/tags/certificates"
|
||||
| "/docs/1.10.0/tags/checklist"
|
||||
| "/docs/1.10.0/tags/code"
|
||||
| "/docs/1.10.0/tags/community"
|
||||
| "/docs/1.10.0/tags/configuration"
|
||||
| "/docs/1.10.0/tags/connections"
|
||||
| "/docs/1.10.0/tags/customization"
|
||||
| "/docs/1.10.0/tags/data-sources"
|
||||
| "/docs/1.10.0/tags/database"
|
||||
| "/docs/1.10.0/tags/developer"
|
||||
| "/docs/1.10.0/tags/development"
|
||||
| "/docs/1.10.0/tags/dns"
|
||||
| "/docs/1.10.0/tags/docker"
|
||||
| "/docs/1.10.0/tags/donation"
|
||||
| "/docs/1.10.0/tags/edit-mode"
|
||||
| "/docs/1.10.0/tags/env"
|
||||
| "/docs/1.10.0/tags/environment-variables"
|
||||
| "/docs/1.10.0/tags/feeds"
|
||||
| "/docs/1.10.0/tags/getting-started"
|
||||
| "/docs/1.10.0/tags/google"
|
||||
| "/docs/1.10.0/tags/grafana"
|
||||
| "/docs/1.10.0/tags/groups"
|
||||
| "/docs/1.10.0/tags/hardware"
|
||||
| "/docs/1.10.0/tags/health"
|
||||
| "/docs/1.10.0/tags/help"
|
||||
| "/docs/1.10.0/tags/icon-picker"
|
||||
| "/docs/1.10.0/tags/icon-repositories"
|
||||
| "/docs/1.10.0/tags/icons"
|
||||
| "/docs/1.10.0/tags/iframe"
|
||||
| "/docs/1.10.0/tags/images"
|
||||
| "/docs/1.10.0/tags/installation"
|
||||
| "/docs/1.10.0/tags/integrade"
|
||||
| "/docs/1.10.0/tags/integration"
|
||||
| "/docs/1.10.0/tags/integrations"
|
||||
| "/docs/1.10.0/tags/interface"
|
||||
| "/docs/1.10.0/tags/jellyserr"
|
||||
| "/docs/1.10.0/tags/layout"
|
||||
| "/docs/1.10.0/tags/ldap"
|
||||
| "/docs/1.10.0/tags/links"
|
||||
| "/docs/1.10.0/tags/lists"
|
||||
| "/docs/1.10.0/tags/management"
|
||||
| "/docs/1.10.0/tags/media"
|
||||
| "/docs/1.10.0/tags/minecraft"
|
||||
| "/docs/1.10.0/tags/monitoring"
|
||||
| "/docs/1.10.0/tags/news"
|
||||
| "/docs/1.10.0/tags/notebook"
|
||||
| "/docs/1.10.0/tags/notes"
|
||||
| "/docs/1.10.0/tags/oidc"
|
||||
| "/docs/1.10.0/tags/open-collective"
|
||||
| "/docs/1.10.0/tags/open-media-vault"
|
||||
| "/docs/1.10.0/tags/overseerr"
|
||||
| "/docs/1.10.0/tags/permissions"
|
||||
| "/docs/1.10.0/tags/pgid"
|
||||
| "/docs/1.10.0/tags/pi-hole"
|
||||
| "/docs/1.10.0/tags/ping"
|
||||
| "/docs/1.10.0/tags/programming"
|
||||
| "/docs/1.10.0/tags/proxmox"
|
||||
| "/docs/1.10.0/tags/proxy"
|
||||
| "/docs/1.10.0/tags/puid"
|
||||
| "/docs/1.10.0/tags/responsive"
|
||||
| "/docs/1.10.0/tags/roles"
|
||||
| "/docs/1.10.0/tags/rss"
|
||||
| "/docs/1.10.0/tags/search"
|
||||
| "/docs/1.10.0/tags/search-engines"
|
||||
| "/docs/1.10.0/tags/security"
|
||||
| "/docs/1.10.0/tags/self-signed"
|
||||
| "/docs/1.10.0/tags/seo"
|
||||
| "/docs/1.10.0/tags/server"
|
||||
| "/docs/1.10.0/tags/settings"
|
||||
| "/docs/1.10.0/tags/sinkhole"
|
||||
| "/docs/1.10.0/tags/sso"
|
||||
| "/docs/1.10.0/tags/system"
|
||||
| "/docs/1.10.0/tags/table"
|
||||
| "/docs/1.10.0/tags/technical-documentation"
|
||||
| "/docs/1.10.0/tags/text"
|
||||
| "/docs/1.10.0/tags/torrent"
|
||||
| "/docs/1.10.0/tags/traefik"
|
||||
| "/docs/1.10.0/tags/translations"
|
||||
| "/docs/1.10.0/tags/unraid"
|
||||
| "/docs/1.10.0/tags/uploads"
|
||||
| "/docs/1.10.0/tags/usenet"
|
||||
| "/docs/1.10.0/tags/users"
|
||||
| "/docs/1.10.0/tags/variables"
|
||||
| "/docs/1.10.0/tags/widgets"
|
||||
| "/docs/1.10.0/advanced/command-line"
|
||||
| "/docs/1.10.0/advanced/command-line/fix-usernames"
|
||||
| "/docs/1.10.0/advanced/command-line/password-recovery"
|
||||
| "/docs/1.10.0/advanced/development/getting-started"
|
||||
| "/docs/1.10.0/advanced/environment-variables"
|
||||
| "/docs/1.10.0/advanced/icons"
|
||||
| "/docs/1.10.0/advanced/keyboard-shortcuts"
|
||||
| "/docs/1.10.0/advanced/proxy"
|
||||
| "/docs/1.10.0/advanced/running-as-different-user"
|
||||
| "/docs/1.10.0/advanced/single-sign-on"
|
||||
| "/docs/1.10.0/category/advanced"
|
||||
| "/docs/1.10.0/category/community"
|
||||
| "/docs/1.10.0/category/developer-guides"
|
||||
| "/docs/1.10.0/category/getting-started"
|
||||
| "/docs/1.10.0/category/installation"
|
||||
| "/docs/1.10.0/category/installation-1"
|
||||
| "/docs/1.10.0/category/integrations"
|
||||
| "/docs/1.10.0/category/management"
|
||||
| "/docs/1.10.0/category/widgets"
|
||||
| "/docs/1.10.0/community/donate"
|
||||
| "/docs/1.10.0/community/faq"
|
||||
| "/docs/1.10.0/community/get-in-touch"
|
||||
| "/docs/1.10.0/community/license"
|
||||
| "/docs/1.10.0/community/translations"
|
||||
| "/docs/1.10.0/getting-started"
|
||||
| "/docs/1.10.0/getting-started/after-the-installation"
|
||||
| "/docs/1.10.0/getting-started/glossary"
|
||||
| "/docs/1.10.0/getting-started/installation/docker"
|
||||
| "/docs/1.10.0/getting-started/installation/easy-panel"
|
||||
| "/docs/1.10.0/getting-started/installation/helm"
|
||||
| "/docs/1.10.0/getting-started/installation/home-assistant"
|
||||
| "/docs/1.10.0/getting-started/installation/portainer"
|
||||
| "/docs/1.10.0/getting-started/installation/qnap"
|
||||
| "/docs/1.10.0/getting-started/installation/railway"
|
||||
| "/docs/1.10.0/getting-started/installation/saltbox"
|
||||
| "/docs/1.10.0/getting-started/installation/source"
|
||||
| "/docs/1.10.0/getting-started/installation/synology"
|
||||
| "/docs/1.10.0/getting-started/installation/unraid"
|
||||
| "/docs/1.10.0/integrations/containers"
|
||||
| "/docs/1.10.0/integrations/dns"
|
||||
| "/docs/1.10.0/integrations/hardware"
|
||||
| "/docs/1.10.0/integrations/media-requester"
|
||||
| "/docs/1.10.0/integrations/media-server"
|
||||
| "/docs/1.10.0/integrations/servarr"
|
||||
| "/docs/1.10.0/integrations/torrent"
|
||||
| "/docs/1.10.0/integrations/usenet"
|
||||
| "/docs/1.10.0/management/api"
|
||||
| "/docs/1.10.0/management/apps"
|
||||
| "/docs/1.10.0/management/boards"
|
||||
| "/docs/1.10.0/management/certificates"
|
||||
| "/docs/1.10.0/management/integrations"
|
||||
| "/docs/1.10.0/management/media"
|
||||
| "/docs/1.10.0/management/search-engines"
|
||||
| "/docs/1.10.0/management/settings"
|
||||
| "/docs/1.10.0/management/users"
|
||||
| "/docs/1.10.0/widgets/bookmarks"
|
||||
| "/docs/1.10.0/widgets/calendar"
|
||||
| "/docs/1.10.0/widgets/clock"
|
||||
| "/docs/1.10.0/widgets/dns-hole"
|
||||
| "/docs/1.10.0/widgets/downloads"
|
||||
| "/docs/1.10.0/widgets/health-monitoring"
|
||||
| "/docs/1.10.0/widgets/home-assistant"
|
||||
| "/docs/1.10.0/widgets/iframe"
|
||||
| "/docs/1.10.0/widgets/indexer-manager"
|
||||
| "/docs/1.10.0/widgets/media-requests"
|
||||
| "/docs/1.10.0/widgets/media-server"
|
||||
| "/docs/1.10.0/widgets/minecraft-server-status"
|
||||
| "/docs/1.10.0/widgets/notebook"
|
||||
| "/docs/1.10.0/widgets/rss"
|
||||
| "/docs/1.10.0/widgets/video"
|
||||
| "/docs/1.10.0/widgets/weather"
|
||||
| "/docs/1.11.0/tags"
|
||||
| "/docs/1.11.0/tags/active-directory"
|
||||
| "/docs/1.11.0/tags/ad-guard"
|
||||
| "/docs/1.11.0/tags/ad-guard-home"
|
||||
| "/docs/1.11.0/tags/administration"
|
||||
| "/docs/1.11.0/tags/advanced"
|
||||
| "/docs/1.11.0/tags/analytics"
|
||||
| "/docs/1.11.0/tags/api"
|
||||
| "/docs/1.11.0/tags/apps"
|
||||
| "/docs/1.11.0/tags/banner"
|
||||
| "/docs/1.11.0/tags/blocking"
|
||||
| "/docs/1.11.0/tags/boards"
|
||||
| "/docs/1.11.0/tags/bookmark"
|
||||
| "/docs/1.11.0/tags/bookmarks"
|
||||
| "/docs/1.11.0/tags/caddy"
|
||||
| "/docs/1.11.0/tags/certificates"
|
||||
| "/docs/1.11.0/tags/checklist"
|
||||
| "/docs/1.11.0/tags/code"
|
||||
| "/docs/1.11.0/tags/community"
|
||||
| "/docs/1.11.0/tags/configuration"
|
||||
| "/docs/1.11.0/tags/connections"
|
||||
| "/docs/1.11.0/tags/customization"
|
||||
| "/docs/1.11.0/tags/data-sources"
|
||||
| "/docs/1.11.0/tags/database"
|
||||
| "/docs/1.11.0/tags/developer"
|
||||
| "/docs/1.11.0/tags/development"
|
||||
| "/docs/1.11.0/tags/dns"
|
||||
| "/docs/1.11.0/tags/docker"
|
||||
| "/docs/1.11.0/tags/donation"
|
||||
| "/docs/1.11.0/tags/edit-mode"
|
||||
| "/docs/1.11.0/tags/env"
|
||||
| "/docs/1.11.0/tags/environment-variables"
|
||||
| "/docs/1.11.0/tags/feeds"
|
||||
| "/docs/1.11.0/tags/getting-started"
|
||||
| "/docs/1.11.0/tags/google"
|
||||
| "/docs/1.11.0/tags/grafana"
|
||||
| "/docs/1.11.0/tags/groups"
|
||||
| "/docs/1.11.0/tags/hardware"
|
||||
| "/docs/1.11.0/tags/health"
|
||||
| "/docs/1.11.0/tags/help"
|
||||
| "/docs/1.11.0/tags/icon-picker"
|
||||
| "/docs/1.11.0/tags/icon-repositories"
|
||||
| "/docs/1.11.0/tags/icons"
|
||||
| "/docs/1.11.0/tags/iframe"
|
||||
| "/docs/1.11.0/tags/images"
|
||||
| "/docs/1.11.0/tags/installation"
|
||||
| "/docs/1.11.0/tags/integrade"
|
||||
| "/docs/1.11.0/tags/integration"
|
||||
| "/docs/1.11.0/tags/integrations"
|
||||
| "/docs/1.11.0/tags/interface"
|
||||
| "/docs/1.11.0/tags/jellyserr"
|
||||
| "/docs/1.11.0/tags/layout"
|
||||
| "/docs/1.11.0/tags/ldap"
|
||||
| "/docs/1.11.0/tags/links"
|
||||
| "/docs/1.11.0/tags/lists"
|
||||
| "/docs/1.11.0/tags/management"
|
||||
| "/docs/1.11.0/tags/media"
|
||||
| "/docs/1.11.0/tags/minecraft"
|
||||
| "/docs/1.11.0/tags/monitoring"
|
||||
| "/docs/1.11.0/tags/news"
|
||||
| "/docs/1.11.0/tags/notebook"
|
||||
| "/docs/1.11.0/tags/notes"
|
||||
| "/docs/1.11.0/tags/oidc"
|
||||
| "/docs/1.11.0/tags/open-collective"
|
||||
| "/docs/1.11.0/tags/open-media-vault"
|
||||
| "/docs/1.11.0/tags/overseerr"
|
||||
| "/docs/1.11.0/tags/permissions"
|
||||
| "/docs/1.11.0/tags/pgid"
|
||||
| "/docs/1.11.0/tags/pi-hole"
|
||||
| "/docs/1.11.0/tags/ping"
|
||||
| "/docs/1.11.0/tags/programming"
|
||||
| "/docs/1.11.0/tags/proxmox"
|
||||
| "/docs/1.11.0/tags/proxy"
|
||||
| "/docs/1.11.0/tags/puid"
|
||||
| "/docs/1.11.0/tags/responsive"
|
||||
| "/docs/1.11.0/tags/roles"
|
||||
| "/docs/1.11.0/tags/rss"
|
||||
| "/docs/1.11.0/tags/search"
|
||||
| "/docs/1.11.0/tags/search-engines"
|
||||
| "/docs/1.11.0/tags/security"
|
||||
| "/docs/1.11.0/tags/self-signed"
|
||||
| "/docs/1.11.0/tags/seo"
|
||||
| "/docs/1.11.0/tags/server"
|
||||
| "/docs/1.11.0/tags/settings"
|
||||
| "/docs/1.11.0/tags/sinkhole"
|
||||
| "/docs/1.11.0/tags/sso"
|
||||
| "/docs/1.11.0/tags/system"
|
||||
| "/docs/1.11.0/tags/table"
|
||||
| "/docs/1.11.0/tags/technical-documentation"
|
||||
| "/docs/1.11.0/tags/text"
|
||||
| "/docs/1.11.0/tags/torrent"
|
||||
| "/docs/1.11.0/tags/traefik"
|
||||
| "/docs/1.11.0/tags/translations"
|
||||
| "/docs/1.11.0/tags/unraid"
|
||||
| "/docs/1.11.0/tags/uploads"
|
||||
| "/docs/1.11.0/tags/usenet"
|
||||
| "/docs/1.11.0/tags/users"
|
||||
| "/docs/1.11.0/tags/variables"
|
||||
| "/docs/1.11.0/tags/widgets"
|
||||
| "/docs/1.11.0/advanced/command-line"
|
||||
| "/docs/1.11.0/advanced/command-line/fix-usernames"
|
||||
| "/docs/1.11.0/advanced/command-line/password-recovery"
|
||||
| "/docs/1.11.0/advanced/development/getting-started"
|
||||
| "/docs/1.11.0/advanced/development/kubernetes"
|
||||
| "/docs/1.11.0/advanced/environment-variables"
|
||||
| "/docs/1.11.0/advanced/icons"
|
||||
| "/docs/1.11.0/advanced/keyboard-shortcuts"
|
||||
| "/docs/1.11.0/advanced/proxy"
|
||||
| "/docs/1.11.0/advanced/running-as-different-user"
|
||||
| "/docs/1.11.0/advanced/single-sign-on"
|
||||
| "/docs/1.11.0/category/advanced"
|
||||
| "/docs/1.11.0/category/community"
|
||||
| "/docs/1.11.0/category/developer-guides"
|
||||
| "/docs/1.11.0/category/getting-started"
|
||||
| "/docs/1.11.0/category/installation"
|
||||
| "/docs/1.11.0/category/installation-1"
|
||||
| "/docs/1.11.0/category/integrations"
|
||||
| "/docs/1.11.0/category/management"
|
||||
| "/docs/1.11.0/category/widgets"
|
||||
| "/docs/1.11.0/community/donate"
|
||||
| "/docs/1.11.0/community/faq"
|
||||
| "/docs/1.11.0/community/get-in-touch"
|
||||
| "/docs/1.11.0/community/license"
|
||||
| "/docs/1.11.0/community/translations"
|
||||
| "/docs/1.11.0/getting-started"
|
||||
| "/docs/1.11.0/getting-started/after-the-installation"
|
||||
| "/docs/1.11.0/getting-started/glossary"
|
||||
| "/docs/1.11.0/getting-started/installation/docker"
|
||||
| "/docs/1.11.0/getting-started/installation/easy-panel"
|
||||
| "/docs/1.11.0/getting-started/installation/helm"
|
||||
| "/docs/1.11.0/getting-started/installation/home-assistant"
|
||||
| "/docs/1.11.0/getting-started/installation/portainer"
|
||||
| "/docs/1.11.0/getting-started/installation/qnap"
|
||||
| "/docs/1.11.0/getting-started/installation/railway"
|
||||
| "/docs/1.11.0/getting-started/installation/saltbox"
|
||||
| "/docs/1.11.0/getting-started/installation/source"
|
||||
| "/docs/1.11.0/getting-started/installation/synology"
|
||||
| "/docs/1.11.0/getting-started/installation/unraid"
|
||||
| "/docs/1.11.0/integrations/containers"
|
||||
| "/docs/1.11.0/integrations/dns"
|
||||
| "/docs/1.11.0/integrations/hardware"
|
||||
| "/docs/1.11.0/integrations/kubernetes"
|
||||
| "/docs/1.11.0/integrations/media-requester"
|
||||
| "/docs/1.11.0/integrations/media-server"
|
||||
| "/docs/1.11.0/integrations/servarr"
|
||||
| "/docs/1.11.0/integrations/torrent"
|
||||
| "/docs/1.11.0/integrations/usenet"
|
||||
| "/docs/1.11.0/management/api"
|
||||
| "/docs/1.11.0/management/apps"
|
||||
| "/docs/1.11.0/management/boards"
|
||||
| "/docs/1.11.0/management/certificates"
|
||||
| "/docs/1.11.0/management/integrations"
|
||||
| "/docs/1.11.0/management/media"
|
||||
| "/docs/1.11.0/management/search-engines"
|
||||
| "/docs/1.11.0/management/settings"
|
||||
| "/docs/1.11.0/management/users"
|
||||
| "/docs/1.11.0/widgets/bookmarks"
|
||||
| "/docs/1.11.0/widgets/calendar"
|
||||
| "/docs/1.11.0/widgets/clock"
|
||||
| "/docs/1.11.0/widgets/dns-hole"
|
||||
| "/docs/1.11.0/widgets/downloads"
|
||||
| "/docs/1.11.0/widgets/health-monitoring"
|
||||
| "/docs/1.11.0/widgets/home-assistant"
|
||||
| "/docs/1.11.0/widgets/iframe"
|
||||
| "/docs/1.11.0/widgets/indexer-manager"
|
||||
| "/docs/1.11.0/widgets/media-requests"
|
||||
| "/docs/1.11.0/widgets/media-server"
|
||||
| "/docs/1.11.0/widgets/minecraft-server-status"
|
||||
| "/docs/1.11.0/widgets/notebook"
|
||||
| "/docs/1.11.0/widgets/rss"
|
||||
| "/docs/1.11.0/widgets/video"
|
||||
| "/docs/1.11.0/widgets/weather"
|
||||
| "/docs/1.12.0/tags"
|
||||
| "/docs/1.12.0/tags/active-directory"
|
||||
| "/docs/1.12.0/tags/ad-guard"
|
||||
| "/docs/1.12.0/tags/ad-guard-home"
|
||||
| "/docs/1.12.0/tags/administration"
|
||||
| "/docs/1.12.0/tags/advanced"
|
||||
| "/docs/1.12.0/tags/analytics"
|
||||
| "/docs/1.12.0/tags/api"
|
||||
| "/docs/1.12.0/tags/apps"
|
||||
| "/docs/1.12.0/tags/banner"
|
||||
| "/docs/1.12.0/tags/blocking"
|
||||
| "/docs/1.12.0/tags/boards"
|
||||
| "/docs/1.12.0/tags/bookmark"
|
||||
| "/docs/1.12.0/tags/bookmarks"
|
||||
| "/docs/1.12.0/tags/caddy"
|
||||
| "/docs/1.12.0/tags/certificates"
|
||||
| "/docs/1.12.0/tags/checklist"
|
||||
| "/docs/1.12.0/tags/code"
|
||||
| "/docs/1.12.0/tags/community"
|
||||
| "/docs/1.12.0/tags/configuration"
|
||||
| "/docs/1.12.0/tags/connections"
|
||||
| "/docs/1.12.0/tags/customization"
|
||||
| "/docs/1.12.0/tags/data-sources"
|
||||
| "/docs/1.12.0/tags/database"
|
||||
| "/docs/1.12.0/tags/developer"
|
||||
| "/docs/1.12.0/tags/development"
|
||||
| "/docs/1.12.0/tags/dns"
|
||||
| "/docs/1.12.0/tags/docker"
|
||||
| "/docs/1.12.0/tags/donation"
|
||||
| "/docs/1.12.0/tags/edit-mode"
|
||||
| "/docs/1.12.0/tags/env"
|
||||
| "/docs/1.12.0/tags/environment-variables"
|
||||
| "/docs/1.12.0/tags/feeds"
|
||||
| "/docs/1.12.0/tags/finance"
|
||||
| "/docs/1.12.0/tags/getting-started"
|
||||
| "/docs/1.12.0/tags/google"
|
||||
| "/docs/1.12.0/tags/grafana"
|
||||
| "/docs/1.12.0/tags/groups"
|
||||
| "/docs/1.12.0/tags/hardware"
|
||||
| "/docs/1.12.0/tags/health"
|
||||
| "/docs/1.12.0/tags/help"
|
||||
| "/docs/1.12.0/tags/icon-picker"
|
||||
| "/docs/1.12.0/tags/icon-repositories"
|
||||
| "/docs/1.12.0/tags/icons"
|
||||
| "/docs/1.12.0/tags/iframe"
|
||||
| "/docs/1.12.0/tags/images"
|
||||
| "/docs/1.12.0/tags/installation"
|
||||
| "/docs/1.12.0/tags/integrade"
|
||||
| "/docs/1.12.0/tags/integration"
|
||||
| "/docs/1.12.0/tags/integrations"
|
||||
| "/docs/1.12.0/tags/interface"
|
||||
| "/docs/1.12.0/tags/jellyserr"
|
||||
| "/docs/1.12.0/tags/layout"
|
||||
| "/docs/1.12.0/tags/ldap"
|
||||
| "/docs/1.12.0/tags/links"
|
||||
| "/docs/1.12.0/tags/lists"
|
||||
| "/docs/1.12.0/tags/management"
|
||||
| "/docs/1.12.0/tags/market"
|
||||
| "/docs/1.12.0/tags/media"
|
||||
| "/docs/1.12.0/tags/minecraft"
|
||||
| "/docs/1.12.0/tags/monitoring"
|
||||
| "/docs/1.12.0/tags/news"
|
||||
| "/docs/1.12.0/tags/notebook"
|
||||
| "/docs/1.12.0/tags/notes"
|
||||
| "/docs/1.12.0/tags/oidc"
|
||||
| "/docs/1.12.0/tags/open-collective"
|
||||
| "/docs/1.12.0/tags/open-media-vault"
|
||||
| "/docs/1.12.0/tags/overseerr"
|
||||
| "/docs/1.12.0/tags/permissions"
|
||||
| "/docs/1.12.0/tags/pgid"
|
||||
| "/docs/1.12.0/tags/pi-hole"
|
||||
| "/docs/1.12.0/tags/ping"
|
||||
| "/docs/1.12.0/tags/programming"
|
||||
| "/docs/1.12.0/tags/proxmox"
|
||||
| "/docs/1.12.0/tags/proxy"
|
||||
| "/docs/1.12.0/tags/puid"
|
||||
| "/docs/1.12.0/tags/responsive"
|
||||
| "/docs/1.12.0/tags/roles"
|
||||
| "/docs/1.12.0/tags/rss"
|
||||
| "/docs/1.12.0/tags/search"
|
||||
| "/docs/1.12.0/tags/search-engines"
|
||||
| "/docs/1.12.0/tags/security"
|
||||
| "/docs/1.12.0/tags/self-signed"
|
||||
| "/docs/1.12.0/tags/seo"
|
||||
| "/docs/1.12.0/tags/server"
|
||||
| "/docs/1.12.0/tags/settings"
|
||||
| "/docs/1.12.0/tags/sinkhole"
|
||||
| "/docs/1.12.0/tags/sso"
|
||||
| "/docs/1.12.0/tags/stocks"
|
||||
| "/docs/1.12.0/tags/system"
|
||||
| "/docs/1.12.0/tags/table"
|
||||
| "/docs/1.12.0/tags/technical-documentation"
|
||||
| "/docs/1.12.0/tags/text"
|
||||
| "/docs/1.12.0/tags/torrent"
|
||||
| "/docs/1.12.0/tags/traefik"
|
||||
| "/docs/1.12.0/tags/translations"
|
||||
| "/docs/1.12.0/tags/unraid"
|
||||
| "/docs/1.12.0/tags/uploads"
|
||||
| "/docs/1.12.0/tags/usenet"
|
||||
| "/docs/1.12.0/tags/users"
|
||||
| "/docs/1.12.0/tags/variables"
|
||||
| "/docs/1.12.0/tags/widgets"
|
||||
| "/docs/1.12.0/advanced/command-line"
|
||||
| "/docs/1.12.0/advanced/command-line/fix-usernames"
|
||||
| "/docs/1.12.0/advanced/command-line/password-recovery"
|
||||
| "/docs/1.12.0/advanced/development/getting-started"
|
||||
| "/docs/1.12.0/advanced/development/kubernetes"
|
||||
| "/docs/1.12.0/advanced/environment-variables"
|
||||
| "/docs/1.12.0/advanced/icons"
|
||||
| "/docs/1.12.0/advanced/keyboard-shortcuts"
|
||||
| "/docs/1.12.0/advanced/proxy"
|
||||
| "/docs/1.12.0/advanced/running-as-different-user"
|
||||
| "/docs/1.12.0/advanced/single-sign-on"
|
||||
| "/docs/1.12.0/category/advanced"
|
||||
| "/docs/1.12.0/category/community"
|
||||
| "/docs/1.12.0/category/developer-guides"
|
||||
| "/docs/1.12.0/category/getting-started"
|
||||
| "/docs/1.12.0/category/installation"
|
||||
| "/docs/1.12.0/category/installation-1"
|
||||
| "/docs/1.12.0/category/integrations"
|
||||
| "/docs/1.12.0/category/management"
|
||||
| "/docs/1.12.0/category/widgets"
|
||||
| "/docs/1.12.0/community/donate"
|
||||
| "/docs/1.12.0/community/faq"
|
||||
| "/docs/1.12.0/community/get-in-touch"
|
||||
| "/docs/1.12.0/community/license"
|
||||
| "/docs/1.12.0/community/translations"
|
||||
| "/docs/1.12.0/getting-started"
|
||||
| "/docs/1.12.0/getting-started/after-the-installation"
|
||||
| "/docs/1.12.0/getting-started/glossary"
|
||||
| "/docs/1.12.0/getting-started/installation/docker"
|
||||
| "/docs/1.12.0/getting-started/installation/easy-panel"
|
||||
| "/docs/1.12.0/getting-started/installation/helm"
|
||||
| "/docs/1.12.0/getting-started/installation/home-assistant"
|
||||
| "/docs/1.12.0/getting-started/installation/portainer"
|
||||
| "/docs/1.12.0/getting-started/installation/proxmox"
|
||||
| "/docs/1.12.0/getting-started/installation/qnap"
|
||||
| "/docs/1.12.0/getting-started/installation/railway"
|
||||
| "/docs/1.12.0/getting-started/installation/saltbox"
|
||||
| "/docs/1.12.0/getting-started/installation/source"
|
||||
| "/docs/1.12.0/getting-started/installation/synology"
|
||||
| "/docs/1.12.0/getting-started/installation/unraid"
|
||||
| "/docs/1.12.0/integrations/containers"
|
||||
| "/docs/1.12.0/integrations/dns"
|
||||
| "/docs/1.12.0/integrations/hardware"
|
||||
| "/docs/1.12.0/integrations/kubernetes"
|
||||
| "/docs/1.12.0/integrations/media-requester"
|
||||
| "/docs/1.12.0/integrations/media-server"
|
||||
| "/docs/1.12.0/integrations/servarr"
|
||||
| "/docs/1.12.0/integrations/torrent"
|
||||
| "/docs/1.12.0/integrations/usenet"
|
||||
| "/docs/1.12.0/management/api"
|
||||
| "/docs/1.12.0/management/apps"
|
||||
| "/docs/1.12.0/management/boards"
|
||||
| "/docs/1.12.0/management/certificates"
|
||||
| "/docs/1.12.0/management/integrations"
|
||||
| "/docs/1.12.0/management/media"
|
||||
| "/docs/1.12.0/management/search-engines"
|
||||
| "/docs/1.12.0/management/settings"
|
||||
| "/docs/1.12.0/management/users"
|
||||
| "/docs/1.12.0/widgets/bookmarks"
|
||||
| "/docs/1.12.0/widgets/calendar"
|
||||
| "/docs/1.12.0/widgets/clock"
|
||||
| "/docs/1.12.0/widgets/dns-hole"
|
||||
| "/docs/1.12.0/widgets/downloads"
|
||||
| "/docs/1.12.0/widgets/health-monitoring"
|
||||
| "/docs/1.12.0/widgets/home-assistant"
|
||||
| "/docs/1.12.0/widgets/iframe"
|
||||
| "/docs/1.12.0/widgets/indexer-manager"
|
||||
| "/docs/1.12.0/widgets/media-requests"
|
||||
| "/docs/1.12.0/widgets/media-server"
|
||||
| "/docs/1.12.0/widgets/minecraft-server-status"
|
||||
| "/docs/1.12.0/widgets/notebook"
|
||||
| "/docs/1.12.0/widgets/rss"
|
||||
| "/docs/1.12.0/widgets/stocks"
|
||||
| "/docs/1.12.0/widgets/video"
|
||||
| "/docs/1.12.0/widgets/weather"
|
||||
| "/docs/next/tags"
|
||||
| "/docs/next/tags/active-directory"
|
||||
| "/docs/next/tags/ad-guard"
|
||||
@@ -70,6 +589,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/next/tags/env"
|
||||
| "/docs/next/tags/environment-variables"
|
||||
| "/docs/next/tags/feeds"
|
||||
| "/docs/next/tags/finance"
|
||||
| "/docs/next/tags/getting-started"
|
||||
| "/docs/next/tags/google"
|
||||
| "/docs/next/tags/grafana"
|
||||
@@ -93,6 +613,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/next/tags/links"
|
||||
| "/docs/next/tags/lists"
|
||||
| "/docs/next/tags/management"
|
||||
| "/docs/next/tags/market"
|
||||
| "/docs/next/tags/media"
|
||||
| "/docs/next/tags/minecraft"
|
||||
| "/docs/next/tags/monitoring"
|
||||
@@ -123,6 +644,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/next/tags/settings"
|
||||
| "/docs/next/tags/sinkhole"
|
||||
| "/docs/next/tags/sso"
|
||||
| "/docs/next/tags/stocks"
|
||||
| "/docs/next/tags/system"
|
||||
| "/docs/next/tags/table"
|
||||
| "/docs/next/tags/technical-documentation"
|
||||
@@ -147,6 +669,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/next/advanced/proxy"
|
||||
| "/docs/next/advanced/running-as-different-user"
|
||||
| "/docs/next/advanced/single-sign-on"
|
||||
| "/docs/next/advanced/styling"
|
||||
| "/docs/next/category/advanced"
|
||||
| "/docs/next/category/community"
|
||||
| "/docs/next/category/developer-guides"
|
||||
@@ -169,12 +692,14 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/next/getting-started/installation/helm"
|
||||
| "/docs/next/getting-started/installation/home-assistant"
|
||||
| "/docs/next/getting-started/installation/portainer"
|
||||
| "/docs/next/getting-started/installation/proxmox"
|
||||
| "/docs/next/getting-started/installation/qnap"
|
||||
| "/docs/next/getting-started/installation/railway"
|
||||
| "/docs/next/getting-started/installation/saltbox"
|
||||
| "/docs/next/getting-started/installation/source"
|
||||
| "/docs/next/getting-started/installation/synology"
|
||||
| "/docs/next/getting-started/installation/unraid"
|
||||
| "/docs/next/integrations/cloud"
|
||||
| "/docs/next/integrations/containers"
|
||||
| "/docs/next/integrations/dns"
|
||||
| "/docs/next/integrations/hardware"
|
||||
@@ -207,6 +732,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/next/widgets/minecraft-server-status"
|
||||
| "/docs/next/widgets/notebook"
|
||||
| "/docs/next/widgets/rss"
|
||||
| "/docs/next/widgets/stocks"
|
||||
| "/docs/next/widgets/video"
|
||||
| "/docs/next/widgets/weather"
|
||||
| "/docs/tags"
|
||||
@@ -242,6 +768,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/tags/env"
|
||||
| "/docs/tags/environment-variables"
|
||||
| "/docs/tags/feeds"
|
||||
| "/docs/tags/finance"
|
||||
| "/docs/tags/getting-started"
|
||||
| "/docs/tags/google"
|
||||
| "/docs/tags/grafana"
|
||||
@@ -265,6 +792,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/tags/links"
|
||||
| "/docs/tags/lists"
|
||||
| "/docs/tags/management"
|
||||
| "/docs/tags/market"
|
||||
| "/docs/tags/media"
|
||||
| "/docs/tags/minecraft"
|
||||
| "/docs/tags/monitoring"
|
||||
@@ -295,6 +823,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/tags/settings"
|
||||
| "/docs/tags/sinkhole"
|
||||
| "/docs/tags/sso"
|
||||
| "/docs/tags/stocks"
|
||||
| "/docs/tags/system"
|
||||
| "/docs/tags/table"
|
||||
| "/docs/tags/technical-documentation"
|
||||
@@ -312,12 +841,14 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/advanced/command-line/fix-usernames"
|
||||
| "/docs/advanced/command-line/password-recovery"
|
||||
| "/docs/advanced/development/getting-started"
|
||||
| "/docs/advanced/development/kubernetes"
|
||||
| "/docs/advanced/environment-variables"
|
||||
| "/docs/advanced/icons"
|
||||
| "/docs/advanced/keyboard-shortcuts"
|
||||
| "/docs/advanced/proxy"
|
||||
| "/docs/advanced/running-as-different-user"
|
||||
| "/docs/advanced/single-sign-on"
|
||||
| "/docs/advanced/styling"
|
||||
| "/docs/category/advanced"
|
||||
| "/docs/category/community"
|
||||
| "/docs/category/developer-guides"
|
||||
@@ -340,15 +871,18 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/getting-started/installation/helm"
|
||||
| "/docs/getting-started/installation/home-assistant"
|
||||
| "/docs/getting-started/installation/portainer"
|
||||
| "/docs/getting-started/installation/proxmox"
|
||||
| "/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/unraid"
|
||||
| "/docs/integrations/cloud"
|
||||
| "/docs/integrations/containers"
|
||||
| "/docs/integrations/dns"
|
||||
| "/docs/integrations/hardware"
|
||||
| "/docs/integrations/kubernetes"
|
||||
| "/docs/integrations/media-requester"
|
||||
| "/docs/integrations/media-server"
|
||||
| "/docs/integrations/servarr"
|
||||
@@ -377,6 +911,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/widgets/minecraft-server-status"
|
||||
| "/docs/widgets/notebook"
|
||||
| "/docs/widgets/rss"
|
||||
| "/docs/widgets/stocks"
|
||||
| "/docs/widgets/video"
|
||||
| "/docs/widgets/weather"
|
||||
| ""
|
||||
|
||||
@@ -3,5 +3,12 @@ import type { HomarrDocumentationPath } from "./homarr-docs-sitemap";
|
||||
const documentationBaseUrl = "https://homarr.dev";
|
||||
|
||||
// Please use the method so the path can be checked!
|
||||
export const createDocumentationLink = (path: HomarrDocumentationPath, hashTag?: `#${string}`) =>
|
||||
`${documentationBaseUrl}${path}${hashTag ?? ""}`;
|
||||
export const createDocumentationLink = (
|
||||
path: HomarrDocumentationPath,
|
||||
hashTag?: `#${string}`,
|
||||
queryParams?: Record<string, string>,
|
||||
) => {
|
||||
const url = `${documentationBaseUrl}${path}`;
|
||||
const params = queryParams ? `?${new URLSearchParams(queryParams)}` : "";
|
||||
return `${url}${params}${hashTag ?? ""}`;
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { createDocumentationLink } from "../docs";
|
||||
import type { HomarrDocumentationPath } from "../docs/homarr-docs-sitemap";
|
||||
|
||||
describe("createDocumentationLink should generate correct URLs", () => {
|
||||
test.each([
|
||||
["/docs/getting-started", undefined, undefined, "https://homarr.dev/docs/getting-started"],
|
||||
["/blog", undefined, undefined, "https://homarr.dev/blog"],
|
||||
["/docs/widgets/weather", "#configuration", undefined, "https://homarr.dev/docs/widgets/weather#configuration"],
|
||||
[
|
||||
"/docs/advanced/environment-variables",
|
||||
undefined,
|
||||
{ lang: "en" },
|
||||
"https://homarr.dev/docs/advanced/environment-variables?lang=en",
|
||||
],
|
||||
[
|
||||
"/docs/widgets/bookmarks",
|
||||
"#sorting",
|
||||
{ lang: "fr", theme: "dark" },
|
||||
"https://homarr.dev/docs/widgets/bookmarks?lang=fr&theme=dark#sorting",
|
||||
],
|
||||
] satisfies [HomarrDocumentationPath, `#${string}` | undefined, Record<string, string> | undefined, string][])(
|
||||
"should create correct URL for path %s with hash %s and params %o",
|
||||
(path, hashTag, queryParams, expected) => {
|
||||
expect(createDocumentationLink(path, hashTag, queryParams)).toBe(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("createDocumentationLink parameter validation", () => {
|
||||
test("should work with only path parameter", () => {
|
||||
const result = createDocumentationLink("/docs/getting-started");
|
||||
expect(result).toBe("https://homarr.dev/docs/getting-started");
|
||||
});
|
||||
|
||||
test("should work with path and hashtag", () => {
|
||||
const result = createDocumentationLink("/docs/getting-started", "#installation");
|
||||
expect(result).toBe("https://homarr.dev/docs/getting-started#installation");
|
||||
});
|
||||
|
||||
test("should work with path and query params", () => {
|
||||
const result = createDocumentationLink("/docs/getting-started", undefined, { version: "1.0" });
|
||||
expect(result).toBe("https://homarr.dev/docs/getting-started?version=1.0");
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ export const widgetKinds = [
|
||||
"dnsHoleControls",
|
||||
"smartHome-entityState",
|
||||
"smartHome-executeAutomation",
|
||||
"stockPrice",
|
||||
"mediaServer",
|
||||
"calendar",
|
||||
"downloads",
|
||||
|
||||
@@ -25,14 +25,14 @@
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/env": "workspace:^0.1.0",
|
||||
"dockerode": "^4.0.4"
|
||||
"dockerode": "^4.0.5"
|
||||
},
|
||||
"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.35",
|
||||
"eslint": "^9.22.0",
|
||||
"@types/dockerode": "^3.3.37",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
Vendored
+1
-1
@@ -30,7 +30,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -26,14 +26,14 @@
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/form": "^7.17.2",
|
||||
"@mantine/form": "^7.17.3",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -29,15 +29,15 @@
|
||||
"@homarr/notifications": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.17.2",
|
||||
"react": "19.0.0",
|
||||
"@mantine/core": "^7.17.3",
|
||||
"react": "19.1.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -33,9 +33,17 @@ interface IconPickerProps {
|
||||
error?: string | null;
|
||||
onFocus?: FocusEventHandler;
|
||||
onBlur?: FocusEventHandler;
|
||||
withAsterisk?: boolean;
|
||||
}
|
||||
|
||||
export const IconPicker = ({ value: propsValue, onChange, error, onFocus, onBlur }: IconPickerProps) => {
|
||||
export const IconPicker = ({
|
||||
value: propsValue,
|
||||
onChange,
|
||||
error,
|
||||
onFocus,
|
||||
onBlur,
|
||||
withAsterisk = true,
|
||||
}: IconPickerProps) => {
|
||||
const [value, setValue] = useUncontrolled({
|
||||
value: propsValue,
|
||||
onChange,
|
||||
@@ -145,7 +153,7 @@ export const IconPicker = ({ value: propsValue, onChange, error, onFocus, onBlur
|
||||
setSearch(value || "");
|
||||
}}
|
||||
rightSectionPointerEvents="none"
|
||||
withAsterisk
|
||||
withAsterisk={withAsterisk}
|
||||
error={error}
|
||||
label={tCommon("iconPicker.label")}
|
||||
placeholder={tCommon("iconPicker.header", { countIcons: String(data?.countIcons ?? 0) })}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import type { ChangeEventHandler } from "react";
|
||||
import { useRef } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import Link from "next/link";
|
||||
import { Button, Checkbox, Collapse, Group, Stack, Textarea, TextInput } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { useDebouncedValue, useDisclosure } from "@mantine/hooks";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
import { IconPicker } from "../icon-picker/icon-picker";
|
||||
import { findBestIconMatch } from "./icon-matcher";
|
||||
|
||||
type FormType = z.infer<typeof validation.app.manage>;
|
||||
|
||||
@@ -45,6 +47,9 @@ export const AppForm = ({
|
||||
},
|
||||
});
|
||||
|
||||
// Debounce the name value with 200ms delay
|
||||
const [debouncedName] = useDebouncedValue(form.values.name, 200);
|
||||
|
||||
const shouldCreateAnother = useRef(false);
|
||||
const handleSubmit = (values: FormType) => {
|
||||
const redirect = !shouldCreateAnother.current;
|
||||
@@ -68,6 +73,25 @@ export const AppForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-select icon based on app name with debounced search
|
||||
const { data: iconsData } = clientApi.icon.findIcons.useQuery(
|
||||
{
|
||||
searchText: debouncedName,
|
||||
},
|
||||
{
|
||||
enabled: debouncedName.length > 3,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedName && !form.values.iconUrl && iconsData?.icons) {
|
||||
const bestMatch = findBestIconMatch(debouncedName, iconsData.icons);
|
||||
if (bestMatch) {
|
||||
form.setFieldValue("iconUrl", bestMatch);
|
||||
}
|
||||
}
|
||||
}, [debouncedName, iconsData]);
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { inferRouterOutputs } from "@trpc/server";
|
||||
|
||||
import type { AppRouter } from "@homarr/api";
|
||||
|
||||
type RouterOutput = inferRouterOutputs<AppRouter>;
|
||||
type IconGroupsOutput = RouterOutput["icon"]["findIcons"]["icons"];
|
||||
|
||||
export const findBestIconMatch = (searchTerm: string, iconGroups: IconGroupsOutput): string | null => {
|
||||
const nameLower = searchTerm.toLowerCase();
|
||||
const allIcons = iconGroups.flatMap((group) => group.icons);
|
||||
|
||||
const getIconPriority = (iconUrl: string) => {
|
||||
const fileName = iconUrl.toLowerCase().split("/").pop()?.split(".")[0];
|
||||
if (!fileName) return -1;
|
||||
|
||||
const isSvg = iconUrl.endsWith(".svg");
|
||||
const isExactMatch = fileName === nameLower;
|
||||
|
||||
if (isExactMatch) return isSvg ? 0 : 1;
|
||||
if (fileName.includes(nameLower)) return isSvg ? 2 : 3;
|
||||
return -1;
|
||||
};
|
||||
|
||||
for (let priority = 0; priority <= 3; priority++) {
|
||||
const match = allIcons.find((icon) => getIconPriority(icon.url) === priority);
|
||||
if (match) return match.url;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -31,7 +31,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@ctrl/deluge": "^7.1.0",
|
||||
"@ctrl/qbittorrent": "^9.4.0",
|
||||
"@ctrl/qbittorrent": "^9.5.2",
|
||||
"@ctrl/transmission": "^7.2.0",
|
||||
"@homarr/certificates": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
@@ -39,7 +39,7 @@
|
||||
"node-ical": "^0.20.1",
|
||||
"proxmox-api": "1.1.1",
|
||||
"tsdav": "^2.1.3",
|
||||
"undici": "7.5.0",
|
||||
"undici": "7.7.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
@@ -48,7 +48,7 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ import objectSupport from "dayjs/plugin/objectSupport";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import * as ical from "node-ical";
|
||||
import { DAVClient } from "tsdav";
|
||||
import type { RequestInit as UndiciFetchRequestInit } from "undici";
|
||||
|
||||
import { createCertificateAgentAsync } from "@homarr/certificates/server";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { Integration } from "../base/integration";
|
||||
@@ -14,12 +16,12 @@ dayjs.extend(objectSupport);
|
||||
|
||||
export class NextcloudIntegration extends Integration {
|
||||
public async testConnectionAsync(): Promise<void> {
|
||||
const client = this.createCalendarClient();
|
||||
const client = await this.createCalendarClientAsync();
|
||||
await client.login();
|
||||
}
|
||||
|
||||
public async getCalendarEventsAsync(start: Date, end: Date): Promise<CalendarEvent[]> {
|
||||
const client = this.createCalendarClient();
|
||||
const client = await this.createCalendarClientAsync();
|
||||
await client.login();
|
||||
|
||||
const calendars = await client.fetchCalendars();
|
||||
@@ -83,7 +85,7 @@ export class NextcloudIntegration extends Integration {
|
||||
});
|
||||
}
|
||||
|
||||
private createCalendarClient() {
|
||||
private async createCalendarClientAsync() {
|
||||
return new DAVClient({
|
||||
serverUrl: this.integration.url,
|
||||
credentials: {
|
||||
@@ -92,6 +94,10 @@ export class NextcloudIntegration extends Integration {
|
||||
},
|
||||
authMethod: "Basic",
|
||||
defaultAccountType: "caldav",
|
||||
fetchOptions: {
|
||||
// We can use the undici options as the global fetch is used instead of the polyfilled.
|
||||
dispatcher: await createCertificateAgentAsync(),
|
||||
} satisfies UndiciFetchRequestInit as RequestInit,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -280,6 +280,7 @@ const mediaInformationSchema = z.union([
|
||||
seasons: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
seasonNumber: z.number(),
|
||||
name: z.string().min(0),
|
||||
episodeCount: z.number().min(0),
|
||||
}),
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { formatMetadata } from "./metadata";
|
||||
|
||||
/**
|
||||
* Formats the cause of an error in the format
|
||||
* @example caused by Error: {message}
|
||||
* {stack-trace}
|
||||
* @param cause next cause in the chain
|
||||
* @param iteration current iteration of the function
|
||||
* @returns formatted and stacked causes
|
||||
*/
|
||||
export const formatErrorCause = (cause: unknown, iteration = 0): string => {
|
||||
// Prevent infinite recursion
|
||||
if (iteration > 5) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (cause instanceof Error) {
|
||||
if (!cause.cause) {
|
||||
return `\ncaused by ${formatErrorTitle(cause)}\n${formatErrorStack(cause.stack)}`;
|
||||
}
|
||||
|
||||
return `\ncaused by ${formatErrorTitle(cause)}\n${formatErrorStack(cause.stack)}${formatErrorCause(cause.cause, iteration + 1)}`;
|
||||
}
|
||||
|
||||
return `\ncaused by ${cause as string}`;
|
||||
};
|
||||
|
||||
const ignoredErrorProperties = ["stack", "message", "name", "cause"];
|
||||
|
||||
/**
|
||||
* Formats the title of an error
|
||||
* @example {name}: {message} {metadata}
|
||||
* @param error error to format title from
|
||||
* @returns formatted error title
|
||||
*/
|
||||
export const formatErrorTitle = (error: Error) => {
|
||||
const title = error.message.length === 0 ? error.name : `${error.name}: ${error.message}`;
|
||||
const metadata = formatMetadata(error, ignoredErrorProperties);
|
||||
|
||||
return `${title} ${metadata}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the stack trance of an error
|
||||
* We remove the first line as it contains the error name and message
|
||||
* @param stack stack trace
|
||||
* @returns formatted stack trace
|
||||
*/
|
||||
export const formatErrorStack = (stack: string | undefined) => (stack ? removeFirstLine(stack) : "");
|
||||
const removeFirstLine = (stack: string) => stack.split("\n").slice(1).join("\n");
|
||||
@@ -2,43 +2,22 @@ import type { transport as Transport } from "winston";
|
||||
import winston, { format, transports } from "winston";
|
||||
|
||||
import { env } from "./env";
|
||||
import { formatErrorCause, formatErrorStack } from "./error";
|
||||
import { formatMetadata } from "./metadata";
|
||||
import { RedisTransport } from "./redis-transport";
|
||||
|
||||
/**
|
||||
* Formats the cause of an error in the format
|
||||
* @example caused by Error: {message}
|
||||
* {stack-trace}
|
||||
* @param cause next cause in the chain
|
||||
* @param iteration current iteration of the function
|
||||
* @returns formatted and stacked causes
|
||||
*/
|
||||
const formatCause = (cause: unknown, iteration = 0): string => {
|
||||
// Prevent infinite recursion
|
||||
if (iteration > 5) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (cause instanceof Error) {
|
||||
if (!cause.cause) {
|
||||
return `\ncaused by ${cause.stack}`;
|
||||
}
|
||||
|
||||
return `\ncaused by ${cause.stack}${formatCause(cause.cause, iteration + 1)}`;
|
||||
}
|
||||
|
||||
return `\ncaused by ${cause as string}`;
|
||||
};
|
||||
|
||||
const logMessageFormat = format.printf(({ level, message, timestamp, cause, stack }) => {
|
||||
const logMessageFormat = format.printf(({ level, message, timestamp, cause, stack, ...metadata }) => {
|
||||
if (!cause && !stack) {
|
||||
return `${timestamp as string} ${level}: ${message as string}`;
|
||||
}
|
||||
|
||||
const formatedStack = formatErrorStack(stack as string | undefined);
|
||||
|
||||
if (!cause) {
|
||||
return `${timestamp as string} ${level}: ${message as string}\n${stack as string}`;
|
||||
return `${timestamp as string} ${level}: ${message as string} ${formatMetadata(metadata)}\n${formatedStack}`;
|
||||
}
|
||||
|
||||
return `${timestamp as string} ${level}: ${message as string}\n${stack as string}${formatCause(cause)}`;
|
||||
return `${timestamp as string} ${level}: ${message as string} ${formatMetadata(metadata)}\n${formatedStack}${formatErrorCause(cause)}`;
|
||||
});
|
||||
|
||||
const logTransports: Transport[] = [new transports.Console()];
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export const formatMetadata = (metadata: Record<string, unknown> | Error, ignoreKeys?: string[]) => {
|
||||
const filteredMetadata = Object.keys(metadata)
|
||||
.filter((key) => !ignoreKeys?.includes(key))
|
||||
.map((key) => ({ key, value: metadata[key as keyof typeof metadata] }))
|
||||
.filter(({ value }) => typeof value !== "object" && typeof value !== "function");
|
||||
|
||||
return filteredMetadata.map(({ key, value }) => `${key}="${value as string}"`).join(" ");
|
||||
};
|
||||
@@ -17,10 +17,8 @@ export class RedisTransport extends Transport {
|
||||
this.emit("logged", info);
|
||||
});
|
||||
|
||||
if (!this.redis) {
|
||||
// Is only initialized here because it did not work when initialized in the constructor or outside the class
|
||||
this.redis = new Redis();
|
||||
}
|
||||
// Is only initialized here because it did not work when initialized in the constructor or outside the class
|
||||
this.redis ??= new Redis();
|
||||
|
||||
this.redis
|
||||
.publish(
|
||||
|
||||
@@ -33,19 +33,19 @@
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.17.2",
|
||||
"@mantine/core": "^7.17.3",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"next": "15.1.7",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"next": "15.2.4",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ export const RequestMediaModal = createModal<RequestMediaModalProps>(({ actions,
|
||||
const anySelected = Object.keys(table.getState().rowSelection).length > 0;
|
||||
|
||||
const handleMutate = () => {
|
||||
const selectedSeasons = table.getSelectedRowModel().rows.flatMap((row) => row.original.id);
|
||||
const selectedSeasons = table.getSelectedRowModel().rows.flatMap((row) => row.original.seasonNumber);
|
||||
mutate({
|
||||
integrationId: innerProps.integrationId,
|
||||
mediaId: innerProps.mediaId,
|
||||
@@ -114,6 +114,7 @@ export const RequestMediaModal = createModal<RequestMediaModalProps>(({ actions,
|
||||
|
||||
interface Season {
|
||||
id: number;
|
||||
seasonNumber: number;
|
||||
name: string;
|
||||
episodeCount: number;
|
||||
}
|
||||
@@ -24,15 +24,15 @@
|
||||
"dependencies": {
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.17.2",
|
||||
"@mantine/hooks": "^7.17.2",
|
||||
"react": "19.0.0"
|
||||
"@mantine/core": "^7.17.3",
|
||||
"@mantine/hooks": "^7.17.3",
|
||||
"react": "19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -24,14 +24,14 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@mantine/notifications": "^7.17.2",
|
||||
"@mantine/notifications": "^7.17.3",
|
||||
"@tabler/icons-react": "^3.31.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -37,12 +37,12 @@
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.17.2",
|
||||
"@mantine/hooks": "^7.17.2",
|
||||
"@mantine/core": "^7.17.3",
|
||||
"@mantine/hooks": "^7.17.3",
|
||||
"adm-zip": "0.5.16",
|
||||
"next": "15.1.7",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"next": "15.2.4",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"superjson": "2.2.2",
|
||||
"zod": "^3.24.2",
|
||||
"zod-form-data": "^2.0.7"
|
||||
@@ -52,7 +52,7 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/adm-zip": "0.5.7",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BoardSize, OldmarrConfig } from "@homarr/old-schema";
|
||||
import type { OldmarrConfig } from "@homarr/old-schema";
|
||||
|
||||
export class OldHomarrImportError extends Error {
|
||||
constructor(oldConfig: OldmarrConfig, cause: unknown) {
|
||||
@@ -7,9 +7,3 @@ export class OldHomarrImportError extends Error {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class OldHomarrScreenSizeError extends Error {
|
||||
constructor(type: "app" | "widget", id: string, screenSize: BoardSize) {
|
||||
super(`Screen size not found for type=${type} id=${id} screenSize=${screenSize}`);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { createId } from "@homarr/db";
|
||||
import { createDbInsertCollectionForTransaction } from "@homarr/db/collection";
|
||||
import { logger } from "@homarr/log";
|
||||
import type { BoardSize } from "@homarr/old-schema";
|
||||
import type { BoardSize, OldmarrConfig } from "@homarr/old-schema";
|
||||
import { boardSizes, getBoardSizeName } from "@homarr/old-schema";
|
||||
|
||||
import { fixSectionIssues } from "../../fix-section-issues";
|
||||
import { OldHomarrImportError } from "../../import-error";
|
||||
import { mapBoard } from "../../mappers/map-board";
|
||||
import { mapBreakpoint } from "../../mappers/map-breakpoint";
|
||||
import { mapColumnCount } from "../../mappers/map-column-count";
|
||||
@@ -57,6 +58,13 @@ export const createBoardInsertCollection = (
|
||||
logger.debug(`Added apps to board insert collection count=${insertCollection.apps.length}`);
|
||||
|
||||
preparedBoards.forEach((board) => {
|
||||
if (!hasEnoughItemShapes(board.config)) {
|
||||
throw new OldHomarrImportError(
|
||||
board.config,
|
||||
new Error("Your config contains items without shapes for all board sizes."),
|
||||
);
|
||||
}
|
||||
|
||||
const { wrappers, categories, wrapperIdsToMerge } = fixSectionIssues(board.config);
|
||||
const { apps, widgets } = moveWidgetsAndAppsIfMerge(board.config, wrapperIdsToMerge, {
|
||||
...settings,
|
||||
@@ -118,3 +126,26 @@ export const createBoardInsertCollection = (
|
||||
|
||||
return insertCollection;
|
||||
};
|
||||
|
||||
export const hasEnoughItemShapes = (config: {
|
||||
apps: Pick<OldmarrConfig["apps"][number], "shape">[];
|
||||
widgets: Pick<OldmarrConfig["widgets"][number], "shape">[];
|
||||
}) => {
|
||||
const invalidSizes: BoardSize[] = [];
|
||||
|
||||
for (const size of boardSizes) {
|
||||
if (invalidSizes.includes(size)) continue;
|
||||
|
||||
if (config.apps.some((app) => app.shape[size] === undefined)) {
|
||||
invalidSizes.push(size);
|
||||
}
|
||||
|
||||
if (invalidSizes.includes(size)) continue;
|
||||
|
||||
if (config.widgets.some((widget) => widget.shape[size] === undefined)) {
|
||||
invalidSizes.push(size);
|
||||
}
|
||||
}
|
||||
|
||||
return invalidSizes.length <= 2;
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import type { BoardSize } from "@homarr/old-schema";
|
||||
|
||||
import { hasEnoughItemShapes } from "../collections/board-collection";
|
||||
|
||||
const defaultShape = {
|
||||
location: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
size: {
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
};
|
||||
|
||||
describe("hasEnoughItemShapes should check if there are more than one shape available for automatic reconstruction", () => {
|
||||
test.each([
|
||||
[true, [], []], // no items, so nothing to check
|
||||
[true, [{ lg: true }], []], // lg always exists
|
||||
[true, [], [{ md: true }]], // md always exists
|
||||
[true, [{ md: true, sm: true }], [{ md: true, lg: true }]], // md always exists
|
||||
[true, [{ md: true }], [{ md: true }]], // md always exists
|
||||
[false, [{ md: true }, { md: true }], [{ lg: true }]], // md is missing for widgets
|
||||
[false, [{ md: true }], [{ lg: true }]], // md is missing for widgets
|
||||
[false, [{ md: true }], [{ md: true, lg: true }, { lg: true }]], // md is missing for 2. widget
|
||||
] as [boolean, Shape[], Shape[]][])(
|
||||
"should return %s if there are more than one shape available",
|
||||
(returnValue, appShapes, widgetShapes) => {
|
||||
const result = hasEnoughItemShapes({
|
||||
apps: appShapes.map((shapes) => ({
|
||||
shape: {
|
||||
sm: shapes.sm ? defaultShape : undefined,
|
||||
md: shapes.md ? defaultShape : undefined,
|
||||
lg: shapes.lg ? defaultShape : undefined,
|
||||
},
|
||||
})),
|
||||
widgets: widgetShapes.map((shapes) => ({
|
||||
shape: {
|
||||
sm: shapes.sm ? defaultShape : undefined,
|
||||
md: shapes.md ? defaultShape : undefined,
|
||||
lg: shapes.lg ? defaultShape : undefined,
|
||||
},
|
||||
})),
|
||||
});
|
||||
|
||||
expect(result).toBe(returnValue);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
type Shape = Partial<Record<BoardSize, true>>;
|
||||
@@ -3,7 +3,6 @@ import { logger } from "@homarr/log";
|
||||
import type { BoardSize, OldmarrApp, OldmarrConfig, OldmarrWidget } from "@homarr/old-schema";
|
||||
import { boardSizes } from "@homarr/old-schema";
|
||||
|
||||
import { OldHomarrScreenSizeError } from "./import-error";
|
||||
import { mapColumnCount } from "./mappers/map-column-count";
|
||||
import type { OldmarrImportConfiguration } from "./settings";
|
||||
|
||||
@@ -60,7 +59,7 @@ export const moveWidgetsAndAppsIfMerge = (
|
||||
for (const screenSize of boardSizes) {
|
||||
const screenSizeShape = app.shape[screenSize];
|
||||
if (!screenSizeShape) {
|
||||
throw new OldHomarrScreenSizeError("app", app.id, screenSize);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the highest widget in the wrapper to increase the offset accordingly
|
||||
@@ -81,7 +80,7 @@ export const moveWidgetsAndAppsIfMerge = (
|
||||
for (const screenSize of boardSizes) {
|
||||
const screenSizeShape = widget.shape[screenSize];
|
||||
if (!screenSizeShape) {
|
||||
throw new OldHomarrScreenSizeError("widget", widget.id, screenSize);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the highest widget in the wrapper to increase the offset accordingly
|
||||
@@ -145,15 +144,6 @@ const moveWidgetsAndAppsInLeftSidebar = (
|
||||
item.area.properties.location === "left" &&
|
||||
(columnCount >= 2 || item.shape[screenSize]?.location.x === 0),
|
||||
update: (item) => {
|
||||
const screenSizeShape = item.shape[screenSize];
|
||||
if (!screenSizeShape) {
|
||||
throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, screenSize);
|
||||
}
|
||||
// Reduce width to one if column count is one
|
||||
if (screenSizeShape.size.width > columnCount) {
|
||||
screenSizeShape.size.width = columnCount;
|
||||
}
|
||||
|
||||
item.area = {
|
||||
type: "wrapper",
|
||||
properties: {
|
||||
@@ -161,6 +151,14 @@ const moveWidgetsAndAppsInLeftSidebar = (
|
||||
},
|
||||
};
|
||||
|
||||
const screenSizeShape = item.shape[screenSize];
|
||||
if (!screenSizeShape) return;
|
||||
|
||||
// Reduce width to one if column count is one
|
||||
if (screenSizeShape.size.width > columnCount) {
|
||||
screenSizeShape.size.width = columnCount;
|
||||
}
|
||||
|
||||
screenSizeShape.location.y += offset;
|
||||
},
|
||||
});
|
||||
@@ -184,11 +182,6 @@ const moveWidgetsAndAppsInLeftSidebar = (
|
||||
item.area.properties.location === "left" &&
|
||||
item.shape[screenSize]?.location.x === 1,
|
||||
update: (item) => {
|
||||
const screenSizeShape = item.shape[screenSize];
|
||||
if (!screenSizeShape) {
|
||||
throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, screenSize);
|
||||
}
|
||||
|
||||
item.area = {
|
||||
type: "wrapper",
|
||||
properties: {
|
||||
@@ -196,6 +189,9 @@ const moveWidgetsAndAppsInLeftSidebar = (
|
||||
},
|
||||
};
|
||||
|
||||
const screenSizeShape = item.shape[screenSize];
|
||||
if (!screenSizeShape) return;
|
||||
|
||||
screenSizeShape.location.x = 0;
|
||||
screenSizeShape.location.y += offset;
|
||||
},
|
||||
@@ -222,16 +218,6 @@ const moveWidgetsAndAppsInRightSidebar = (
|
||||
item.area.properties.location === "right" &&
|
||||
(columnCount >= 2 || item.shape[screenSize]?.location.x === 0),
|
||||
update: (item) => {
|
||||
const screenSizeShape = item.shape[screenSize];
|
||||
if (!screenSizeShape) {
|
||||
throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, screenSize);
|
||||
}
|
||||
|
||||
// Reduce width to one if column count is one
|
||||
if (screenSizeShape.size.width > columnCount) {
|
||||
screenSizeShape.size.width = columnCount;
|
||||
}
|
||||
|
||||
item.area = {
|
||||
type: "wrapper",
|
||||
properties: {
|
||||
@@ -239,6 +225,14 @@ const moveWidgetsAndAppsInRightSidebar = (
|
||||
},
|
||||
};
|
||||
|
||||
const screenSizeShape = item.shape[screenSize];
|
||||
if (!screenSizeShape) return;
|
||||
|
||||
// Reduce width to one if column count is one
|
||||
if (screenSizeShape.size.width > columnCount) {
|
||||
screenSizeShape.size.width = columnCount;
|
||||
}
|
||||
|
||||
screenSizeShape.location.y += offset;
|
||||
screenSizeShape.location.x += xOffsetDelta;
|
||||
},
|
||||
@@ -260,11 +254,6 @@ const moveWidgetsAndAppsInRightSidebar = (
|
||||
item.area.properties.location === "left" &&
|
||||
item.shape[screenSize]?.location.x === 1,
|
||||
update: (item) => {
|
||||
const screenSizeShape = item.shape[screenSize];
|
||||
if (!screenSizeShape) {
|
||||
throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, screenSize);
|
||||
}
|
||||
|
||||
item.area = {
|
||||
type: "wrapper",
|
||||
properties: {
|
||||
@@ -272,6 +261,9 @@ const moveWidgetsAndAppsInRightSidebar = (
|
||||
},
|
||||
};
|
||||
|
||||
const screenSizeShape = item.shape[screenSize];
|
||||
if (!screenSizeShape) return;
|
||||
|
||||
screenSizeShape.location.x = 0;
|
||||
screenSizeShape.location.y += offset;
|
||||
},
|
||||
@@ -312,16 +304,15 @@ const updateItems = (options: {
|
||||
for (const item of items) {
|
||||
const before = createItemSnapshot(item, options.screenSize);
|
||||
|
||||
options.update(item);
|
||||
|
||||
const screenSizeShape = item.shape[options.screenSize];
|
||||
if (!screenSizeShape) {
|
||||
throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, options.screenSize);
|
||||
}
|
||||
if (!screenSizeShape) return requiredHeight;
|
||||
|
||||
if (screenSizeShape.location.y + screenSizeShape.size.height > requiredHeight) {
|
||||
requiredHeight = screenSizeShape.location.y + screenSizeShape.size.height;
|
||||
}
|
||||
|
||||
options.update(item);
|
||||
const after = createItemSnapshot(item, options.screenSize);
|
||||
|
||||
logger.debug(
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import { formatError } from "pretty-print-error";
|
||||
import type { fetch } from "undici";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { extractErrorMessage } from "@homarr/common";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
export const sendPingRequestAsync = async (url: string) => {
|
||||
try {
|
||||
return await fetchWithTimeoutAndCertificates(url).then((response) => ({ statusCode: response.status }));
|
||||
} catch (error) {
|
||||
logger.error("packages/ping/src/index.ts:", formatError(error));
|
||||
logger.error(new Error(`Failed to send ping request to "${url}"`, { cause: error }));
|
||||
return {
|
||||
error: formatError(error),
|
||||
error: extractErrorMessage(error),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { formatError } from "pretty-print-error";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
import { hashObjectBase64, Stopwatch } from "@homarr/common";
|
||||
@@ -107,7 +106,9 @@ export const createRequestIntegrationJobHandler = <
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to run integration job integration=${integrationId} inputHash='${inputHash}' error=${formatError(error)}`,
|
||||
new Error(`Failed to run integration job integration=${integrationId} inputHash='${inputHash}'`, {
|
||||
cause: error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import dayjs from "dayjs";
|
||||
import { z } from "zod";
|
||||
|
||||
import { fetchWithTimeout } from "@homarr/common";
|
||||
|
||||
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
|
||||
|
||||
export const fetchStockPriceHandler = createCachedWidgetRequestHandler({
|
||||
queryKey: "fetchStockPriceResult",
|
||||
widgetKind: "stockPrice",
|
||||
async requestAsync(input: { stock: string; timeRange: string; timeInterval: string }) {
|
||||
const response = await fetchWithTimeout(
|
||||
`https://query1.finance.yahoo.com/v8/finance/chart/${input.stock}?range=${input.timeRange}&interval=${input.timeInterval}`,
|
||||
);
|
||||
const data = dataSchema.parse(await response.json());
|
||||
|
||||
if ("error" in data) {
|
||||
throw new Error(data.error.description);
|
||||
}
|
||||
if (data.chart.result.length !== 1) {
|
||||
throw new Error("Received multiple results");
|
||||
}
|
||||
if (!data.chart.result[0]) {
|
||||
throw new Error("Received invalid data");
|
||||
}
|
||||
|
||||
return data.chart.result[0];
|
||||
},
|
||||
cacheDuration: dayjs.duration(5, "minutes"),
|
||||
});
|
||||
|
||||
const dataSchema = z
|
||||
.object({
|
||||
error: z.object({
|
||||
description: z.string(),
|
||||
}),
|
||||
})
|
||||
.or(
|
||||
z.object({
|
||||
chart: z.object({
|
||||
result: z.array(
|
||||
z.object({
|
||||
indicators: z.object({
|
||||
quote: z.array(
|
||||
z.object({
|
||||
close: z.array(z.number()),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
meta: z.object({
|
||||
symbol: z.string(),
|
||||
shortName: z.string(),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -2,6 +2,7 @@ import dayjs from "dayjs";
|
||||
import { Octokit } from "octokit";
|
||||
import { compareSemVer, isValidSemVer } from "semver-parser";
|
||||
|
||||
import { fetchWithTimeout } from "@homarr/common";
|
||||
import { logger } from "@homarr/log";
|
||||
import { createChannelWithLatestAndEvents } from "@homarr/redis";
|
||||
import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler";
|
||||
@@ -12,7 +13,11 @@ export const updateCheckerRequestHandler = createCachedRequestHandler({
|
||||
queryKey: "homarr-update-checker",
|
||||
cacheDuration: dayjs.duration(1, "hour"),
|
||||
async requestAsync(_) {
|
||||
const octokit = new Octokit();
|
||||
const octokit = new Octokit({
|
||||
request: {
|
||||
fetch: fetchWithTimeout,
|
||||
},
|
||||
});
|
||||
const releases = await octokit.rest.repos.listReleases({
|
||||
owner: "homarr-labs",
|
||||
repo: "homarr",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -25,16 +25,16 @@
|
||||
"@homarr/api": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@mantine/dates": "^7.17.2",
|
||||
"next": "15.1.7",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0"
|
||||
"@mantine/dates": "^7.17.3",
|
||||
"next": "15.2.4",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -33,21 +33,21 @@
|
||||
"@homarr/settings": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.17.2",
|
||||
"@mantine/hooks": "^7.17.2",
|
||||
"@mantine/spotlight": "^7.17.2",
|
||||
"@mantine/core": "^7.17.3",
|
||||
"@mantine/hooks": "^7.17.3",
|
||||
"@mantine/spotlight": "^7.17.3",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"jotai": "^2.12.2",
|
||||
"next": "15.1.7",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"next": "15.2.4",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"use-deep-compare-effect": "^1.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -32,16 +32,16 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"deepmerge": "4.3.1",
|
||||
"mantine-react-table": "2.0.0-beta.9",
|
||||
"next": "15.1.7",
|
||||
"next": "15.2.4",
|
||||
"next-intl": "4.0.2",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0"
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -60,9 +60,31 @@ export const localeConfigurations = {
|
||||
return import("dayjs/locale/de").then((module) => module.default);
|
||||
},
|
||||
},
|
||||
"de-CH": {
|
||||
name: "Deutsch (Schweiz)",
|
||||
translatedName: "German (Swiss)",
|
||||
flagIcon: "ch",
|
||||
importMrtLocalization() {
|
||||
return import("mantine-react-table/locales/de/index.esm.mjs").then((module) => module.MRT_Localization_DE);
|
||||
},
|
||||
importDayJsLocale() {
|
||||
return import("dayjs/locale/de-ch").then((module) => module.default);
|
||||
},
|
||||
},
|
||||
"en-gb": {
|
||||
name: "English (UK)",
|
||||
translatedName: "English (UK)",
|
||||
flagIcon: "gb",
|
||||
importMrtLocalization() {
|
||||
return import("mantine-react-table/locales/en/index.esm.mjs").then((module) => module.MRT_Localization_EN);
|
||||
},
|
||||
importDayJsLocale() {
|
||||
return import("dayjs/locale/en-gb").then((module) => module.default);
|
||||
},
|
||||
},
|
||||
en: {
|
||||
name: "English",
|
||||
translatedName: "English",
|
||||
name: "English (US)",
|
||||
translatedName: "English (US)",
|
||||
flagIcon: "us",
|
||||
importMrtLocalization() {
|
||||
return import("mantine-react-table/locales/en/index.esm.mjs").then((module) => module.MRT_Localization_EN);
|
||||
|
||||
@@ -980,6 +980,9 @@
|
||||
"remove": ""
|
||||
},
|
||||
"option": {
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"borderColor": {
|
||||
"label": ""
|
||||
}
|
||||
@@ -1424,6 +1427,82 @@
|
||||
"run": ""
|
||||
}
|
||||
},
|
||||
"stockPrice": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"option": {
|
||||
"stock": {
|
||||
"label": ""
|
||||
},
|
||||
"timeRange": {
|
||||
"label": "",
|
||||
"option": {
|
||||
"1d": {
|
||||
"label": ""
|
||||
},
|
||||
"5d": {
|
||||
"label": ""
|
||||
},
|
||||
"1mo": {
|
||||
"label": ""
|
||||
},
|
||||
"3mo": {
|
||||
"label": ""
|
||||
},
|
||||
"6mo": {
|
||||
"label": ""
|
||||
},
|
||||
"ytd": {
|
||||
"label": ""
|
||||
},
|
||||
"1y": {
|
||||
"label": ""
|
||||
},
|
||||
"2y": {
|
||||
"label": ""
|
||||
},
|
||||
"5y": {
|
||||
"label": ""
|
||||
},
|
||||
"10y": {
|
||||
"label": ""
|
||||
},
|
||||
"max": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeInterval": {
|
||||
"label": "",
|
||||
"option": {
|
||||
"5m": {
|
||||
"label": ""
|
||||
},
|
||||
"15m": {
|
||||
"label": ""
|
||||
},
|
||||
"30m": {
|
||||
"label": ""
|
||||
},
|
||||
"1h": {
|
||||
"label": ""
|
||||
},
|
||||
"1d": {
|
||||
"label": ""
|
||||
},
|
||||
"5d": {
|
||||
"label": ""
|
||||
},
|
||||
"1wk": {
|
||||
"label": ""
|
||||
},
|
||||
"1mo": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"calendar": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
|
||||
@@ -677,11 +677,11 @@
|
||||
"description": "集成“{kind}”可以与搜索引擎一起使用。勾选此项可自动配置搜索引擎。"
|
||||
},
|
||||
"createApp": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "创建应用",
|
||||
"description": "创建一个具有与集成相同名称和图标的应用程序。 留空下面的输入字段使用集成URL创建应用程序。"
|
||||
},
|
||||
"appHref": {
|
||||
"placeholder": ""
|
||||
"placeholder": "自定义应用URL"
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
@@ -980,6 +980,9 @@
|
||||
"remove": "删除动态部分"
|
||||
},
|
||||
"option": {
|
||||
"title": {
|
||||
"label": "标题"
|
||||
},
|
||||
"borderColor": {
|
||||
"label": "边界颜色"
|
||||
}
|
||||
@@ -1424,6 +1427,82 @@
|
||||
"run": "运行 {name}"
|
||||
}
|
||||
},
|
||||
"stockPrice": {
|
||||
"name": "股票价格",
|
||||
"description": "显示公司的当前股价",
|
||||
"option": {
|
||||
"stock": {
|
||||
"label": "股票代码"
|
||||
},
|
||||
"timeRange": {
|
||||
"label": "时间范围",
|
||||
"option": {
|
||||
"1d": {
|
||||
"label": "1天"
|
||||
},
|
||||
"5d": {
|
||||
"label": "5天"
|
||||
},
|
||||
"1mo": {
|
||||
"label": "1个月"
|
||||
},
|
||||
"3mo": {
|
||||
"label": "3 个月"
|
||||
},
|
||||
"6mo": {
|
||||
"label": "6 个月"
|
||||
},
|
||||
"ytd": {
|
||||
"label": "年初至今"
|
||||
},
|
||||
"1y": {
|
||||
"label": "1 年"
|
||||
},
|
||||
"2y": {
|
||||
"label": "2 年"
|
||||
},
|
||||
"5y": {
|
||||
"label": "5 年"
|
||||
},
|
||||
"10y": {
|
||||
"label": "10 年"
|
||||
},
|
||||
"max": {
|
||||
"label": "最大"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeInterval": {
|
||||
"label": "时间间隔",
|
||||
"option": {
|
||||
"5m": {
|
||||
"label": "5 分钟"
|
||||
},
|
||||
"15m": {
|
||||
"label": "15 分钟"
|
||||
},
|
||||
"30m": {
|
||||
"label": "30 分钟"
|
||||
},
|
||||
"1h": {
|
||||
"label": "1 小时"
|
||||
},
|
||||
"1d": {
|
||||
"label": "1 天"
|
||||
},
|
||||
"5d": {
|
||||
"label": "5 天"
|
||||
},
|
||||
"1wk": {
|
||||
"label": "1 周"
|
||||
},
|
||||
"1mo": {
|
||||
"label": "1 个月"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"calendar": {
|
||||
"name": "日历",
|
||||
"description": "在日历视图中显示某个相对时间段内的集成事件",
|
||||
|
||||
Loaded 100 of 148 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user