Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93b9c4f6f3 | ||
|
|
1def17f4b5 | ||
|
|
b62d47d303 | ||
|
|
b71c5c6fc7 | ||
|
|
2bdb1c75f6 | ||
|
|
347c6d1519 | ||
|
|
d6180ebc73 | ||
|
|
395e03c810 | ||
|
|
f26634745e | ||
|
|
0ab2ae4cbb | ||
|
|
0d5f430bd4 | ||
|
|
dd300c2e1f | ||
|
|
8cb9ddbce3 | ||
|
|
e94c05f3d1 | ||
|
|
b6eb8ca0f6 | ||
|
|
9b6110b9c9 | ||
|
|
273d4edb23 | ||
|
|
36bd2a51b5 | ||
|
|
44853ea64b | ||
|
|
680f7b0c72 | ||
|
|
7c0baf5fba | ||
|
|
57264a5d6c | ||
|
|
7ec1793040 | ||
|
|
2568a9cecd | ||
|
|
861728a7c3 | ||
|
|
0284c5ec39 | ||
|
|
3315053ba1 | ||
|
|
fce8bfe1f4 | ||
|
|
cb50eda8fb | ||
|
|
bee1dddc9f | ||
|
|
0e7eae4553 | ||
|
|
c36e1708be | ||
|
|
f1c001d728 | ||
|
|
8a0557f927 | ||
|
|
4aa6fbb42d | ||
|
|
e5e686d590 | ||
|
|
758b1a0490 | ||
|
|
d512fc042a | ||
|
|
d401086d1b | ||
|
|
76ce6ff1fa | ||
|
|
9bb26ebcc3 | ||
|
|
a87bb54404 | ||
|
|
a9aa0e303b | ||
|
|
dbae2d2fdd | ||
|
|
b9b2398cd6 | ||
|
|
82c5361112 | ||
|
|
2b1c433cef | ||
|
|
a70b0b7047 | ||
|
|
e148698593 | ||
|
|
0d1b40c381 | ||
|
|
a934529146 | ||
|
|
0c9c5b8955 | ||
|
|
4dfde2ba05 | ||
|
|
48aef9e862 | ||
|
|
547a8c34c5 | ||
|
|
9006d3d2fc | ||
|
|
a2934247b2 | ||
|
|
1a07fa9af5 | ||
|
|
ec11c738f2 | ||
|
|
254c248e12 | ||
|
|
70686a0066 | ||
|
|
3299cedc04 | ||
|
|
1012cad0e3 | ||
|
|
b10f33bf0b | ||
|
|
a2dbac5ad7 | ||
|
|
f39a21c296 | ||
|
|
284ea239f7 | ||
|
|
583a6b6f4c | ||
|
|
039fad643e | ||
|
|
2f2c00ba3d | ||
|
|
ebb3012a65 | ||
|
|
f4ce76ac13 |
No files matched your search
@@ -31,6 +31,7 @@ body:
|
||||
label: Version
|
||||
description: What version of Homarr are you running?
|
||||
options:
|
||||
- 1.19.0
|
||||
- 1.18.0
|
||||
- 1.17.0
|
||||
- 1.16.0
|
||||
|
||||
@@ -1,3 +1,57 @@
|
||||
## [1.19.1](https://github.com/homarr-labs/homarr/compare/v1.19.0...v1.19.1) (2025-05-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency @extractus/feed-extractor to v7.1.5 ([#3020](https://github.com/homarr-labs/homarr/issues/3020)) ([758b1a0](https://github.com/homarr-labs/homarr/commit/758b1a049019d903f152f783eab3b6fc4be4de14))
|
||||
* **deps:** update dependency @kubernetes/client-node to ^1.2.0 ([#3042](https://github.com/homarr-labs/homarr/issues/3042)) ([273d4ed](https://github.com/homarr-labs/homarr/commit/273d4edb233f5d5faa7105c23208d95268119786))
|
||||
* **deps:** update dependency better-sqlite3 to ^11.10.0 ([#3053](https://github.com/homarr-labs/homarr/issues/3053)) ([0d5f430](https://github.com/homarr-labs/homarr/commit/0d5f430bd45e6d29d604d4f93ff57cfc7a64e309))
|
||||
* **deps:** update dependency drizzle-kit to ^0.31.1 ([#3013](https://github.com/homarr-labs/homarr/issues/3013)) ([76ce6ff](https://github.com/homarr-labs/homarr/commit/76ce6ff1fab34223e7ff5abf46193f1ae9b011d1))
|
||||
* **deps:** update dependency eslint-config-prettier to ^10.1.3 ([#3047](https://github.com/homarr-labs/homarr/issues/3047)) ([e94c05f](https://github.com/homarr-labs/homarr/commit/e94c05f3d1eea1841b978359d3c41d8a46a08a39))
|
||||
* **deps:** update dependency eslint-config-prettier to ^10.1.5 ([#3061](https://github.com/homarr-labs/homarr/issues/3061)) ([395e03c](https://github.com/homarr-labs/homarr/commit/395e03c8101af873f35388ae1b833dc68d552792))
|
||||
* **deps:** update dependency jotai to ^2.12.4 ([#3038](https://github.com/homarr-labs/homarr/issues/3038)) ([7ec1793](https://github.com/homarr-labs/homarr/commit/7ec179304040d6782e516358aeaae2eaa143dc1a))
|
||||
* **deps:** update dependency ldapts to v8 ([#3028](https://github.com/homarr-labs/homarr/issues/3028)) ([7c0baf5](https://github.com/homarr-labs/homarr/commit/7c0baf5fba19735c2b3fac0728359a9aa8c617e6))
|
||||
* **deps:** update dependency react-error-boundary to v6 ([#3016](https://github.com/homarr-labs/homarr/issues/3016)) ([680f7b0](https://github.com/homarr-labs/homarr/commit/680f7b0c7277ffcbb6a033fd7a2f14146e8a2fca))
|
||||
* **deps:** update dependency typescript-eslint to ^8.32.0 ([#3027](https://github.com/homarr-labs/homarr/issues/3027)) ([bee1ddd](https://github.com/homarr-labs/homarr/commit/bee1dddc9ff1b213ef9fd9cc1696bf22cc816b19))
|
||||
* **deps:** update dependency ws to ^8.18.2 ([#3012](https://github.com/homarr-labs/homarr/issues/3012)) ([9bb26eb](https://github.com/homarr-labs/homarr/commit/9bb26ebcc3a3f400317c85bd1fb13621d52049c2))
|
||||
* **deps:** update dependency zod to ^3.24.4 ([#3021](https://github.com/homarr-labs/homarr/issues/3021)) ([e5e686d](https://github.com/homarr-labs/homarr/commit/e5e686d59086320a565c3367a389d1117fd74d88))
|
||||
* **deps:** update mantine monorepo to v8 (major) ([#3022](https://github.com/homarr-labs/homarr/issues/3022)) ([44853ea](https://github.com/homarr-labs/homarr/commit/44853ea64bed3a093a26cb07904d8beedf220e4e))
|
||||
* **deps:** update nextjs monorepo to v15.3.2 ([#3045](https://github.com/homarr-labs/homarr/issues/3045)) ([b6eb8ca](https://github.com/homarr-labs/homarr/commit/b6eb8ca0f6d0e6b2fac40423a6c0157dff292622))
|
||||
* **deps:** update tanstack-query monorepo to ^5.75.2 ([#3018](https://github.com/homarr-labs/homarr/issues/3018)) ([d512fc0](https://github.com/homarr-labs/homarr/commit/d512fc042a0cb06238f3c86f92b12ad152208a9c))
|
||||
* **deps:** update tanstack-query monorepo to ^5.75.4 ([#3029](https://github.com/homarr-labs/homarr/issues/3029)) ([861728a](https://github.com/homarr-labs/homarr/commit/861728a7c3156bbe618b9fb186d9f168ba9b339b))
|
||||
* **deps:** update tanstack-query monorepo to ^5.75.5 ([#3048](https://github.com/homarr-labs/homarr/issues/3048)) ([8cb9ddb](https://github.com/homarr-labs/homarr/commit/8cb9ddbce3f00e6e3df07c796c39fda7dd18b7b1))
|
||||
* **deps:** update tanstack-query monorepo to ^5.75.6 ([#3062](https://github.com/homarr-labs/homarr/issues/3062)) ([d6180eb](https://github.com/homarr-labs/homarr/commit/d6180ebc7387249265ad9c296c52aab6d393d88a))
|
||||
* **deps:** update tanstack-query monorepo to ^5.75.7 ([#3063](https://github.com/homarr-labs/homarr/issues/3063)) ([b71c5c6](https://github.com/homarr-labs/homarr/commit/b71c5c6fc73cab4ebc1bfe9d70722f166ebcdb47))
|
||||
* **deps:** update tiptap monorepo to v2.12.0 ([#3025](https://github.com/homarr-labs/homarr/issues/3025)) ([8a0557f](https://github.com/homarr-labs/homarr/commit/8a0557f927f6423e37a372c4c4ffea224598ff45))
|
||||
* **deps:** update turbo monorepo to ^2.5.3 ([#3043](https://github.com/homarr-labs/homarr/issues/3043)) ([9b6110b](https://github.com/homarr-labs/homarr/commit/9b6110b9c9ec08bde335cc95d7548e2dddfab45b))
|
||||
* **logs:** log error cause when object, sign in oidc debug messages ([#3054](https://github.com/homarr-labs/homarr/issues/3054)) ([2bdb1c7](https://github.com/homarr-labs/homarr/commit/2bdb1c75f680e7715bbe68c6f50e24ea861302ca))
|
||||
* **releases-widget:** error display, decouple database repository object from responses and batch widget queries ([#2891](https://github.com/homarr-labs/homarr/issues/2891)) ([347c6d1](https://github.com/homarr-labs/homarr/commit/347c6d15191a64899ff844cfe689973f44b9c556))
|
||||
* **releases-widget:** improved provider parsing of missing properties ([#2964](https://github.com/homarr-labs/homarr/issues/2964)) ([b62d47d](https://github.com/homarr-labs/homarr/commit/b62d47d303ca1271b932b6347a489df6f16fc680))
|
||||
|
||||
## [1.19.0](https://github.com/homarr-labs/homarr/compare/v1.18.1...v1.19.0) (2025-05-02)
|
||||
|
||||
### Features
|
||||
|
||||
* **widget:** add prefetch for apps and bookmarks ([#2895](https://github.com/homarr-labs/homarr/issues/2895)) ([82c5361](https://github.com/homarr-labs/homarr/commit/82c53611122dd07e5639a902afa0a6a4fd8ed4e5))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cookie:** parsing of header not working with percentage char ([#2989](https://github.com/homarr-labs/homarr/issues/2989)) ([547a8c3](https://github.com/homarr-labs/homarr/commit/547a8c34c58c88e6f0074df327f49aadf9ab07f4))
|
||||
* **deps:** update dependency @t3-oss/env-nextjs to ^0.13.4 ([#2993](https://github.com/homarr-labs/homarr/issues/2993)) ([1a07fa9](https://github.com/homarr-labs/homarr/commit/1a07fa9af59e93f9ce5d70b72855bb070f5b5787))
|
||||
* **deps:** update dependency tsx to v4.19.4 ([#2981](https://github.com/homarr-labs/homarr/issues/2981)) ([a2dbac5](https://github.com/homarr-labs/homarr/commit/a2dbac5ad73aa50af7f4b40076bb5fcfa61adf80))
|
||||
* **deps:** update dependency typescript-eslint to ^8.31.1 ([#2979](https://github.com/homarr-labs/homarr/issues/2979)) ([284ea23](https://github.com/homarr-labs/homarr/commit/284ea239f71f49c74e3ea60fa5de2fd968f1675b))
|
||||
* **deps:** update mantine monorepo to ^7.17.7 ([#3004](https://github.com/homarr-labs/homarr/issues/3004)) ([a70b0b7](https://github.com/homarr-labs/homarr/commit/a70b0b70479df8fff8a2d256995844bad1fb55a0))
|
||||
* **deps:** update tanstack-query monorepo to ^5.74.11 ([#2984](https://github.com/homarr-labs/homarr/issues/2984)) ([3299ced](https://github.com/homarr-labs/homarr/commit/3299cedc04cd87c7f014f69a900e7c6e6c33d954))
|
||||
* **deps:** update tanstack-query monorepo to ^5.74.7 ([#2924](https://github.com/homarr-labs/homarr/issues/2924)) ([f4ce76a](https://github.com/homarr-labs/homarr/commit/f4ce76ac13136c2f559d3aff019dcc2add7c98dd))
|
||||
* **deps:** update tanstack-query monorepo to ^5.74.9 ([#2983](https://github.com/homarr-labs/homarr/issues/2983)) ([1012cad](https://github.com/homarr-labs/homarr/commit/1012cad0e32f43f46bbb4e89ade3f0eb7fa54313))
|
||||
* **deps:** update tanstack-query monorepo to ^5.75.0 ([#2998](https://github.com/homarr-labs/homarr/issues/2998)) ([0d1b40c](https://github.com/homarr-labs/homarr/commit/0d1b40c381fc068c770480f81c6c7dcefce0ec86))
|
||||
* **deps:** update tanstack-query monorepo to ^5.75.1 ([#3006](https://github.com/homarr-labs/homarr/issues/3006)) ([2b1c433](https://github.com/homarr-labs/homarr/commit/2b1c433cefe667d930ce5178cc1218ffb7fa43c0))
|
||||
* **deps:** update testcontainers-node monorepo to ^10.25.0 ([#2997](https://github.com/homarr-labs/homarr/issues/2997)) ([a934529](https://github.com/homarr-labs/homarr/commit/a934529146a0f53a047d2e3e834145c62c8ee891))
|
||||
* **deps:** update tiptap monorepo to v2.11.9 ([#2987](https://github.com/homarr-labs/homarr/issues/2987)) ([70686a0](https://github.com/homarr-labs/homarr/commit/70686a00662a7f690efbb45920e5c45ad4a866ef))
|
||||
* **deps:** update trpc monorepo to ^11.1.2 ([#2982](https://github.com/homarr-labs/homarr/issues/2982)) ([b10f33b](https://github.com/homarr-labs/homarr/commit/b10f33bf0b976217b2076ce84d8d1d9219b36e4c))
|
||||
* **media-request:** add missing completed status ([#2990](https://github.com/homarr-labs/homarr/issues/2990)) ([9006d3d](https://github.com/homarr-labs/homarr/commit/9006d3d2fc21be5ba2eae616e467113356c91ebe))
|
||||
* **releases-widget:** improve ui for smaller widgets ([#2962](https://github.com/homarr-labs/homarr/issues/2962)) ([f39a21c](https://github.com/homarr-labs/homarr/commit/f39a21c2962530298274ee705f3d85da023ea3e0))
|
||||
* **unifi:** port is ignored ([#2995](https://github.com/homarr-labs/homarr/issues/2995)) ([0c9c5b8](https://github.com/homarr-labs/homarr/commit/0c9c5b8955c883191e5b6b137660c1a3cbd16ce4))
|
||||
|
||||
## [1.18.1](https://github.com/homarr-labs/homarr/compare/v1.18.0...v1.18.1) (2025-04-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
+21
-21
@@ -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.5",
|
||||
"@mantine/core": "^7.17.5",
|
||||
"@mantine/dropzone": "^7.17.5",
|
||||
"@mantine/hooks": "^7.17.5",
|
||||
"@mantine/modals": "^7.17.5",
|
||||
"@mantine/tiptap": "^7.17.5",
|
||||
"@mantine/colors-generator": "^8.0.0",
|
||||
"@mantine/core": "^8.0.0",
|
||||
"@mantine/dropzone": "^8.0.0",
|
||||
"@mantine/hooks": "^8.0.0",
|
||||
"@mantine/modals": "^8.0.0",
|
||||
"@mantine/tiptap": "^8.0.0",
|
||||
"@million/lint": "1.0.14",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"@tanstack/react-query": "^5.74.4",
|
||||
"@tanstack/react-query-devtools": "^5.74.4",
|
||||
"@tanstack/react-query-next-experimental": "^5.74.4",
|
||||
"@trpc/client": "^11.1.1",
|
||||
"@trpc/next": "^11.1.1",
|
||||
"@trpc/react-query": "^11.1.1",
|
||||
"@trpc/server": "^11.1.1",
|
||||
"@tanstack/react-query": "^5.75.7",
|
||||
"@tanstack/react-query-devtools": "^5.75.7",
|
||||
"@tanstack/react-query-next-experimental": "^5.75.7",
|
||||
"@trpc/client": "^11.1.2",
|
||||
"@trpc/next": "^11.1.2",
|
||||
"@trpc/react-query": "^11.1.2",
|
||||
"@trpc/server": "^11.1.2",
|
||||
"@xterm/addon-canvas": "^0.7.0",
|
||||
"@xterm/addon-fit": "0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
@@ -72,33 +72,33 @@
|
||||
"dotenv": "^16.5.0",
|
||||
"flag-icons": "^7.3.2",
|
||||
"glob": "^11.0.2",
|
||||
"jotai": "^2.12.3",
|
||||
"jotai": "^2.12.4",
|
||||
"mantine-react-table": "2.0.0-beta.9",
|
||||
"next": "15.3.1",
|
||||
"next": "15.3.2",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-simple-code-editor": "^0.14.1",
|
||||
"sass": "^1.87.0",
|
||||
"superjson": "2.2.2",
|
||||
"swagger-ui-react": "^5.21.0",
|
||||
"use-deep-compare-effect": "^1.8.1",
|
||||
"zod": "^3.24.3"
|
||||
"zod": "^3.24.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/chroma-js": "3.1.1",
|
||||
"@types/node": "^22.15.2",
|
||||
"@types/node": "^22.15.17",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "19.1.2",
|
||||
"@types/react-dom": "19.1.2",
|
||||
"@types/react": "19.1.3",
|
||||
"@types/react-dom": "19.1.3",
|
||||
"@types/swagger-ui-react": "^5.18.0",
|
||||
"concurrently": "^9.1.2",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"node-loader": "^2.1.0",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.3"
|
||||
|
||||
@@ -4,15 +4,21 @@ import { TRPCError } from "@trpc/server";
|
||||
// Placed here because gridstack styles are used for board content
|
||||
import "~/styles/gridstack.scss";
|
||||
|
||||
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
|
||||
|
||||
import { getQueryClient } from "@homarr/api/server";
|
||||
import { IntegrationProvider } from "@homarr/auth/client";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { getIntegrationsWithPermissionsAsync } from "@homarr/auth/server";
|
||||
import { isNullOrWhitespace } from "@homarr/common";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
import { getI18n } from "@homarr/translation/server";
|
||||
import { prefetchForKindAsync } from "@homarr/widgets/prefetch";
|
||||
|
||||
import { createMetaTitle } from "~/metadata";
|
||||
import { createBoardLayout } from "../_layout-creator";
|
||||
import type { Board } from "../_types";
|
||||
import type { Board, Item } from "../_types";
|
||||
import { DynamicClientBoard } from "./_dynamic-client";
|
||||
import { BoardContentHeaderActions } from "./_header-actions";
|
||||
|
||||
@@ -31,14 +37,36 @@ export const createBoardContentPage = <TParams extends Record<string, unknown>>(
|
||||
getInitialBoardAsync: getInitialBoard,
|
||||
}),
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
page: async () => {
|
||||
page: async ({ params }: { params: Promise<TParams> }) => {
|
||||
const session = await auth();
|
||||
const integrations = await getIntegrationsWithPermissionsAsync(session);
|
||||
|
||||
const board = await getInitialBoard(await params);
|
||||
const queryClient = getQueryClient();
|
||||
|
||||
// Prefetch item data
|
||||
const itemsMap = board.items.reduce((acc, item) => {
|
||||
const existing = acc.get(item.kind);
|
||||
if (existing) {
|
||||
existing.push(item);
|
||||
} else {
|
||||
acc.set(item.kind, [item]);
|
||||
}
|
||||
return acc;
|
||||
}, new Map<WidgetKind, Item[]>());
|
||||
|
||||
for (const [kind, items] of itemsMap) {
|
||||
await prefetchForKindAsync(kind, queryClient, items).catch((error) => {
|
||||
logger.error(new Error("Failed to prefetch widget", { cause: error }));
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<IntegrationProvider integrations={integrations}>
|
||||
<DynamicClientBoard />
|
||||
</IntegrationProvider>
|
||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||
<IntegrationProvider integrations={integrations}>
|
||||
<DynamicClientBoard />
|
||||
</IntegrationProvider>
|
||||
</HydrationBoundary>
|
||||
);
|
||||
},
|
||||
generateMetadataAsync: async ({ params }: { params: Promise<TParams> }): Promise<Metadata> => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import "@homarr/ui/styles.css";
|
||||
import "~/styles/scroll-area.scss";
|
||||
|
||||
import { notFound } from "next/navigation";
|
||||
import type { DayOfWeek } from "@mantine/dates";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
@@ -87,7 +88,15 @@ export default async function Layout(props: {
|
||||
},
|
||||
(innerProps) => (
|
||||
<SettingsProvider
|
||||
user={user}
|
||||
user={
|
||||
user
|
||||
? {
|
||||
...user,
|
||||
// Convert type, because output schema is not smart enough to infer $type from drizzle
|
||||
firstDayOfWeek: user.firstDayOfWeek as DayOfWeek,
|
||||
}
|
||||
: null
|
||||
}
|
||||
serverSettings={{
|
||||
board: {
|
||||
homeBoardId: serverSettings.board.homeBoardId,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import type { SettingsContextProps } from "@homarr/settings";
|
||||
import type { SettingsContextProps } from "@homarr/settings/creator";
|
||||
import type { WidgetComponentProps } from "@homarr/widgets";
|
||||
import { reduceWidgetOptionsWithDefaultValues } from "@homarr/widgets";
|
||||
|
||||
|
||||
@@ -44,11 +44,11 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node": "^22.15.2",
|
||||
"@types/node": "^22.15.17",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"prettier": "^3.5.3",
|
||||
"tsx": "4.19.3",
|
||||
"tsx": "4.19.4",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -26,15 +26,15 @@
|
||||
"@homarr/redis": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"tsx": "4.19.3",
|
||||
"ws": "^8.18.1"
|
||||
"tsx": "4.19.4",
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
|
||||
+8
-11
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homarr",
|
||||
"version": "1.18.1",
|
||||
"version": "1.19.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "cross-env CI=true turbo build",
|
||||
@@ -38,22 +38,22 @@
|
||||
"@semantic-release/github": "^11.0.2",
|
||||
"@semantic-release/npm": "^12.0.1",
|
||||
"@semantic-release/release-notes-generator": "^14.0.3",
|
||||
"@turbo/gen": "^2.5.2",
|
||||
"@turbo/gen": "^2.5.3",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"@vitest/coverage-v8": "^3.1.2",
|
||||
"@vitest/ui": "^3.1.2",
|
||||
"@vitest/coverage-v8": "^3.1.3",
|
||||
"@vitest/ui": "^3.1.3",
|
||||
"conventional-changelog-conventionalcommits": "^8.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"jsdom": "^26.1.0",
|
||||
"prettier": "^3.5.3",
|
||||
"semantic-release": "^24.2.3",
|
||||
"testcontainers": "^10.24.2",
|
||||
"turbo": "^2.5.2",
|
||||
"testcontainers": "^10.25.0",
|
||||
"turbo": "^2.5.3",
|
||||
"typescript": "^5.8.3",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.1.2"
|
||||
"vitest": "^3.1.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.9.0",
|
||||
"packageManager": "pnpm@10.10.0",
|
||||
"engines": {
|
||||
"node": ">=22.15.0"
|
||||
},
|
||||
@@ -72,9 +72,6 @@
|
||||
"overrides": {
|
||||
"proxmox-api>undici": "7.8.0"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"pretty-print-error": "patches/pretty-print-error.patch"
|
||||
},
|
||||
"allowUnusedPatches": true,
|
||||
"ignoredBuiltDependencies": [
|
||||
"@scarf/scarf",
|
||||
|
||||
@@ -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.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -40,24 +40,25 @@
|
||||
"@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.2",
|
||||
"@trpc/client": "^11.1.1",
|
||||
"@trpc/react-query": "^11.1.1",
|
||||
"@trpc/server": "^11.1.1",
|
||||
"@kubernetes/client-node": "^1.2.0",
|
||||
"@tanstack/react-query": "^5.75.7",
|
||||
"@trpc/client": "^11.1.2",
|
||||
"@trpc/react-query": "^11.1.2",
|
||||
"@trpc/server": "^11.1.2",
|
||||
"@trpc/tanstack-react-query": "^11.1.2",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"next": "15.3.1",
|
||||
"pretty-print-error": "^1.1.2",
|
||||
"next": "15.3.2",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"superjson": "2.2.2",
|
||||
"trpc-to-openapi": "^2.2.0",
|
||||
"zod": "^3.24.3"
|
||||
"zod": "^3.24.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export type IntegrationAction = "query" | "interact";
|
||||
* @param action query for showing data or interact for mutating data
|
||||
* @param kinds kinds of integrations that are supported
|
||||
* @returns middleware that can be used with trpc
|
||||
* @example publicProcedure.unstable_concat(createOneIntegrationMiddleware("query", "piHole", "homeAssistant")).query(...)
|
||||
* @example publicProcedure.concat(createOneIntegrationMiddleware("query", "piHole", "homeAssistant")).query(...)
|
||||
* @throws TRPCError NOT_FOUND if the integration was not found
|
||||
* @throws TRPCError FORBIDDEN if the user does not have permission to perform the specified action on the specified integration
|
||||
*/
|
||||
@@ -82,7 +82,7 @@ export const createOneIntegrationMiddleware = <TKind extends IntegrationKind>(
|
||||
* @param action query for showing data or interact for mutating data
|
||||
* @param kinds kinds of integrations that are supported
|
||||
* @returns middleware that can be used with trpc
|
||||
* @example publicProcedure.unstable_concat(createManyIntegrationMiddleware("query", "piHole", "homeAssistant")).query(...)
|
||||
* @example publicProcedure.concat(createManyIntegrationMiddleware("query", "piHole", "homeAssistant")).query(...)
|
||||
* @throws TRPCError NOT_FOUND if the integration was not found
|
||||
* @throws TRPCError FORBIDDEN if the user does not have permission to perform the specified action on at least one of the specified integrations
|
||||
*/
|
||||
|
||||
@@ -18,7 +18,7 @@ const dockerCache = createCacheChannel<{
|
||||
export const dockerRouter = createTRPCRouter({
|
||||
getContainers: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(dockerMiddleware())
|
||||
.concat(dockerMiddleware())
|
||||
.query(async () => {
|
||||
const result = await dockerCache
|
||||
.consumeAsync(async () => {
|
||||
@@ -81,14 +81,14 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
invalidate: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(dockerMiddleware())
|
||||
.concat(dockerMiddleware())
|
||||
.mutation(async () => {
|
||||
await dockerCache.invalidateAsync();
|
||||
return;
|
||||
}),
|
||||
startAll: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(dockerMiddleware())
|
||||
.concat(dockerMiddleware())
|
||||
.input(z.object({ ids: z.array(z.string()) }))
|
||||
.mutation(async ({ input }) => {
|
||||
await Promise.allSettled(
|
||||
@@ -102,7 +102,7 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
stopAll: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(dockerMiddleware())
|
||||
.concat(dockerMiddleware())
|
||||
.input(z.object({ ids: z.array(z.string()) }))
|
||||
.mutation(async ({ input }) => {
|
||||
await Promise.allSettled(
|
||||
@@ -116,7 +116,7 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
restartAll: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(dockerMiddleware())
|
||||
.concat(dockerMiddleware())
|
||||
.input(z.object({ ids: z.array(z.string()) }))
|
||||
.mutation(async ({ input }) => {
|
||||
await Promise.allSettled(
|
||||
@@ -130,7 +130,7 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
removeAll: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(dockerMiddleware())
|
||||
.concat(dockerMiddleware())
|
||||
.input(z.object({ ids: z.array(z.string()) }))
|
||||
.mutation(async ({ input }) => {
|
||||
await Promise.allSettled(
|
||||
|
||||
@@ -467,7 +467,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
searchInIntegration: protectedProcedure
|
||||
.unstable_concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("search")))
|
||||
.concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("search")))
|
||||
.input(z.object({ integrationId: z.string(), query: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const integrationInstance = await createIntegrationAsync(ctx.integration);
|
||||
|
||||
@@ -13,7 +13,7 @@ import { MemoryResourceParser } from "../resource-parser/memory-resource-parser"
|
||||
export const clusterRouter = createTRPCRouter({
|
||||
getCluster: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(kubernetesMiddleware())
|
||||
.concat(kubernetesMiddleware())
|
||||
.query(async (): Promise<KubernetesCluster> => {
|
||||
const { coreApi, metricsApi, versionApi, kubeConfig } = KubernetesClient.getInstance();
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { KubernetesClient } from "../kubernetes-client";
|
||||
export const configMapsRouter = createTRPCRouter({
|
||||
getConfigMaps: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(kubernetesMiddleware())
|
||||
.concat(kubernetesMiddleware())
|
||||
.query(async (): Promise<KubernetesBaseResource[]> => {
|
||||
const { coreApi } = KubernetesClient.getInstance();
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { KubernetesClient } from "../kubernetes-client";
|
||||
export const ingressesRouter = createTRPCRouter({
|
||||
getIngresses: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(kubernetesMiddleware())
|
||||
.concat(kubernetesMiddleware())
|
||||
.query(async (): Promise<KubernetesIngress[]> => {
|
||||
const { networkingApi } = KubernetesClient.getInstance();
|
||||
try {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { KubernetesClient } from "../kubernetes-client";
|
||||
export const namespacesRouter = createTRPCRouter({
|
||||
getNamespaces: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(kubernetesMiddleware())
|
||||
.concat(kubernetesMiddleware())
|
||||
.query(async (): Promise<KubernetesNamespace[]> => {
|
||||
const { coreApi } = KubernetesClient.getInstance();
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { MemoryResourceParser } from "../resource-parser/memory-resource-parser"
|
||||
export const nodesRouter = createTRPCRouter({
|
||||
getNodes: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(kubernetesMiddleware())
|
||||
.concat(kubernetesMiddleware())
|
||||
.query(async (): Promise<KubernetesNode[]> => {
|
||||
const { coreApi, metricsApi } = KubernetesClient.getInstance();
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { KubernetesClient } from "../kubernetes-client";
|
||||
export const podsRouter = createTRPCRouter({
|
||||
getPods: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(kubernetesMiddleware())
|
||||
.concat(kubernetesMiddleware())
|
||||
.query(async (): Promise<KubernetesPod[]> => {
|
||||
const { coreApi, kubeConfig } = KubernetesClient.getInstance();
|
||||
try {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { KubernetesClient } from "../kubernetes-client";
|
||||
export const secretsRouter = createTRPCRouter({
|
||||
getSecrets: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(kubernetesMiddleware())
|
||||
.concat(kubernetesMiddleware())
|
||||
.query(async (): Promise<KubernetesSecret[]> => {
|
||||
const { coreApi } = KubernetesClient.getInstance();
|
||||
try {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { KubernetesClient } from "../kubernetes-client";
|
||||
export const servicesRouter = createTRPCRouter({
|
||||
getServices: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(kubernetesMiddleware())
|
||||
.concat(kubernetesMiddleware())
|
||||
.query(async (): Promise<KubernetesService[]> => {
|
||||
const { coreApi } = KubernetesClient.getInstance();
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { KubernetesClient } from "../kubernetes-client";
|
||||
export const volumesRouter = createTRPCRouter({
|
||||
getVolumes: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(kubernetesMiddleware())
|
||||
.concat(kubernetesMiddleware())
|
||||
.query(async (): Promise<KubernetesVolume[]> => {
|
||||
const { coreApi } = KubernetesClient.getInstance();
|
||||
|
||||
|
||||
@@ -133,14 +133,14 @@ export const searchEngineRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
getMediaRequestOptions: protectedProcedure
|
||||
.unstable_concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr"))
|
||||
.concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr"))
|
||||
.input(mediaRequestOptionsSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const integration = await createIntegrationAsync(ctx.integration);
|
||||
return await integration.getSeriesInformationAsync(input.mediaType, input.mediaId);
|
||||
}),
|
||||
requestMedia: protectedProcedure
|
||||
.unstable_concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr"))
|
||||
.concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr"))
|
||||
.input(mediaRequestRequestSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const integration = await createIntegrationAsync(ctx.integration);
|
||||
|
||||
@@ -17,7 +17,7 @@ export const calendarRouter = createTRPCRouter({
|
||||
showUnmonitored: z.boolean(),
|
||||
}),
|
||||
)
|
||||
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("calendar")))
|
||||
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("calendar")))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const results = await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
|
||||
@@ -14,7 +14,7 @@ import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trp
|
||||
|
||||
export const dnsHoleRouter = createTRPCRouter({
|
||||
summary: publicProcedure
|
||||
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("dnsHole")))
|
||||
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("dnsHole")))
|
||||
.query(async ({ ctx }) => {
|
||||
const results = await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
@@ -36,7 +36,7 @@ export const dnsHoleRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
subscribeToSummary: publicProcedure
|
||||
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("dnsHole")))
|
||||
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("dnsHole")))
|
||||
.subscription(({ ctx }) => {
|
||||
return observable<{
|
||||
integration: Modify<Integration, { kind: IntegrationKindByCategory<"dnsHole"> }>;
|
||||
@@ -63,7 +63,7 @@ export const dnsHoleRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
enable: protectedProcedure
|
||||
.unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole")))
|
||||
.concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole")))
|
||||
.mutation(async ({ ctx: { integration } }) => {
|
||||
const client = await createIntegrationAsync(integration);
|
||||
await client.enableAsync();
|
||||
@@ -81,7 +81,7 @@ export const dnsHoleRouter = createTRPCRouter({
|
||||
duration: z.number().optional(),
|
||||
}),
|
||||
)
|
||||
.unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole")))
|
||||
.concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole")))
|
||||
.mutation(async ({ ctx: { integration }, input }) => {
|
||||
const client = await createIntegrationAsync(integration);
|
||||
await client.disableAsync(input.duration);
|
||||
|
||||
@@ -18,7 +18,7 @@ const createDownloadClientIntegrationMiddleware = (action: IntegrationAction) =>
|
||||
|
||||
export const downloadsRouter = createTRPCRouter({
|
||||
getJobsAndStatuses: publicProcedure
|
||||
.unstable_concat(createDownloadClientIntegrationMiddleware("query"))
|
||||
.concat(createDownloadClientIntegrationMiddleware("query"))
|
||||
.query(async ({ ctx }) => {
|
||||
return await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
@@ -39,7 +39,7 @@ export const downloadsRouter = createTRPCRouter({
|
||||
);
|
||||
}),
|
||||
subscribeToJobsAndStatuses: publicProcedure
|
||||
.unstable_concat(createDownloadClientIntegrationMiddleware("query"))
|
||||
.concat(createDownloadClientIntegrationMiddleware("query"))
|
||||
.subscription(({ ctx }) => {
|
||||
return observable<{
|
||||
integration: Modify<Integration, { kind: IntegrationKindByCategory<"downloadClient"> }>;
|
||||
@@ -64,18 +64,16 @@ export const downloadsRouter = createTRPCRouter({
|
||||
};
|
||||
});
|
||||
}),
|
||||
pause: protectedProcedure
|
||||
.unstable_concat(createDownloadClientIntegrationMiddleware("interact"))
|
||||
.mutation(async ({ ctx }) => {
|
||||
await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
const integrationInstance = await createIntegrationAsync(integration);
|
||||
await integrationInstance.pauseQueueAsync();
|
||||
}),
|
||||
);
|
||||
}),
|
||||
pause: protectedProcedure.concat(createDownloadClientIntegrationMiddleware("interact")).mutation(async ({ ctx }) => {
|
||||
await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
const integrationInstance = await createIntegrationAsync(integration);
|
||||
await integrationInstance.pauseQueueAsync();
|
||||
}),
|
||||
);
|
||||
}),
|
||||
pauseItem: protectedProcedure
|
||||
.unstable_concat(createDownloadClientIntegrationMiddleware("interact"))
|
||||
.concat(createDownloadClientIntegrationMiddleware("interact"))
|
||||
.input(z.object({ item: downloadClientItemSchema }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await Promise.all(
|
||||
@@ -85,18 +83,16 @@ export const downloadsRouter = createTRPCRouter({
|
||||
}),
|
||||
);
|
||||
}),
|
||||
resume: protectedProcedure
|
||||
.unstable_concat(createDownloadClientIntegrationMiddleware("interact"))
|
||||
.mutation(async ({ ctx }) => {
|
||||
await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
const integrationInstance = await createIntegrationAsync(integration);
|
||||
await integrationInstance.resumeQueueAsync();
|
||||
}),
|
||||
);
|
||||
}),
|
||||
resume: protectedProcedure.concat(createDownloadClientIntegrationMiddleware("interact")).mutation(async ({ ctx }) => {
|
||||
await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
const integrationInstance = await createIntegrationAsync(integration);
|
||||
await integrationInstance.resumeQueueAsync();
|
||||
}),
|
||||
);
|
||||
}),
|
||||
resumeItem: protectedProcedure
|
||||
.unstable_concat(createDownloadClientIntegrationMiddleware("interact"))
|
||||
.concat(createDownloadClientIntegrationMiddleware("interact"))
|
||||
.input(z.object({ item: downloadClientItemSchema }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await Promise.all(
|
||||
@@ -107,7 +103,7 @@ export const downloadsRouter = createTRPCRouter({
|
||||
);
|
||||
}),
|
||||
deleteItem: protectedProcedure
|
||||
.unstable_concat(createDownloadClientIntegrationMiddleware("interact"))
|
||||
.concat(createDownloadClientIntegrationMiddleware("interact"))
|
||||
.input(z.object({ item: downloadClientItemSchema, fromDisk: z.boolean() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await Promise.all(
|
||||
|
||||
@@ -9,7 +9,7 @@ import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const healthMonitoringRouter = createTRPCRouter({
|
||||
getSystemHealthStatus: publicProcedure
|
||||
.unstable_concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot"))
|
||||
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot"))
|
||||
.query(async ({ ctx }) => {
|
||||
return await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
@@ -26,7 +26,7 @@ export const healthMonitoringRouter = createTRPCRouter({
|
||||
);
|
||||
}),
|
||||
subscribeSystemHealthStatus: publicProcedure
|
||||
.unstable_concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot"))
|
||||
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot"))
|
||||
.subscription(({ ctx }) => {
|
||||
return observable<{ integrationId: string; healthInfo: HealthMonitoring; timestamp: Date }>((emit) => {
|
||||
const unsubscribes: (() => void)[] = [];
|
||||
@@ -49,14 +49,14 @@ export const healthMonitoringRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
getClusterHealthStatus: publicProcedure
|
||||
.unstable_concat(createOneIntegrationMiddleware("query", "proxmox"))
|
||||
.concat(createOneIntegrationMiddleware("query", "proxmox"))
|
||||
.query(async ({ ctx }) => {
|
||||
const innerHandler = clusterInfoRequestHandler.handler(ctx.integration, {});
|
||||
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||
return data;
|
||||
}),
|
||||
subscribeClusterHealthStatus: publicProcedure
|
||||
.unstable_concat(createOneIntegrationMiddleware("query", "proxmox"))
|
||||
.concat(createOneIntegrationMiddleware("query", "proxmox"))
|
||||
.subscription(({ ctx }) => {
|
||||
return observable<ProxmoxClusterInfo>((emit) => {
|
||||
const unsubscribes: (() => void)[] = [];
|
||||
|
||||
@@ -16,7 +16,7 @@ const createIndexerManagerIntegrationMiddleware = (action: IntegrationAction) =>
|
||||
|
||||
export const indexerManagerRouter = createTRPCRouter({
|
||||
getIndexersStatus: publicProcedure
|
||||
.unstable_concat(createIndexerManagerIntegrationMiddleware("query"))
|
||||
.concat(createIndexerManagerIntegrationMiddleware("query"))
|
||||
.query(async ({ ctx }) => {
|
||||
const results = await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
@@ -33,7 +33,7 @@ export const indexerManagerRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
subscribeIndexersStatus: publicProcedure
|
||||
.unstable_concat(createIndexerManagerIntegrationMiddleware("query"))
|
||||
.concat(createIndexerManagerIntegrationMiddleware("query"))
|
||||
.subscription(({ ctx }) => {
|
||||
return observable<{ integrationId: string; indexers: Indexer[] }>((emit) => {
|
||||
const unsubscribes: (() => void)[] = [];
|
||||
@@ -55,7 +55,7 @@ export const indexerManagerRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
testAllIndexers: protectedProcedure
|
||||
.unstable_concat(createIndexerManagerIntegrationMiddleware("interact"))
|
||||
.concat(createIndexerManagerIntegrationMiddleware("interact"))
|
||||
.mutation(async ({ ctx }) => {
|
||||
await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trp
|
||||
|
||||
export const mediaRequestsRouter = createTRPCRouter({
|
||||
getLatestRequests: publicProcedure
|
||||
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest")))
|
||||
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest")))
|
||||
.query(async ({ ctx }) => {
|
||||
const results = await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
@@ -39,7 +39,7 @@ export const mediaRequestsRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
subscribeToLatestRequests: publicProcedure
|
||||
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest")))
|
||||
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest")))
|
||||
.subscription(({ ctx }) => {
|
||||
return observable<{
|
||||
integrationId: string;
|
||||
@@ -65,7 +65,7 @@ export const mediaRequestsRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
getStats: publicProcedure
|
||||
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest")))
|
||||
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest")))
|
||||
.query(async ({ ctx }) => {
|
||||
const results = await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
@@ -91,7 +91,7 @@ export const mediaRequestsRouter = createTRPCRouter({
|
||||
};
|
||||
}),
|
||||
answerRequest: protectedProcedure
|
||||
.unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("mediaRequest")))
|
||||
.concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("mediaRequest")))
|
||||
.input(z.object({ requestId: z.number(), answer: z.enum(["approve", "decline"]) }))
|
||||
.mutation(async ({ ctx: { integration }, input }) => {
|
||||
const integrationInstance = await createIntegrationAsync(integration);
|
||||
|
||||
@@ -14,7 +14,7 @@ const createMediaServerIntegrationMiddleware = (action: IntegrationAction) =>
|
||||
|
||||
export const mediaServerRouter = createTRPCRouter({
|
||||
getCurrentStreams: publicProcedure
|
||||
.unstable_concat(createMediaServerIntegrationMiddleware("query"))
|
||||
.concat(createMediaServerIntegrationMiddleware("query"))
|
||||
.input(z.object({ showOnlyPlaying: z.boolean() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
return await Promise.all(
|
||||
@@ -32,7 +32,7 @@ export const mediaServerRouter = createTRPCRouter({
|
||||
);
|
||||
}),
|
||||
subscribeToCurrentStreams: publicProcedure
|
||||
.unstable_concat(createMediaServerIntegrationMiddleware("query"))
|
||||
.concat(createMediaServerIntegrationMiddleware("query"))
|
||||
.input(z.object({ showOnlyPlaying: z.boolean() }))
|
||||
.subscription(({ ctx, input }) => {
|
||||
return observable<{ integrationId: string; data: StreamSession[] }>((emit) => {
|
||||
|
||||
@@ -11,7 +11,7 @@ const createIndexerManagerIntegrationMiddleware = (action: IntegrationAction) =>
|
||||
|
||||
export const mediaTranscodingRouter = createTRPCRouter({
|
||||
getDataAsync: publicProcedure
|
||||
.unstable_concat(createIndexerManagerIntegrationMiddleware("query"))
|
||||
.concat(createIndexerManagerIntegrationMiddleware("query"))
|
||||
.input(paginatedSchema.pick({ page: true, pageSize: true }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const innerHandler = mediaTranscodingRequestHandler.handler(ctx.integration, {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const networkControllerRouter = createTRPCRouter({
|
||||
summary: publicProcedure
|
||||
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("networkController")))
|
||||
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("networkController")))
|
||||
.query(async ({ ctx }) => {
|
||||
const results = await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
@@ -34,7 +34,7 @@ export const networkControllerRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
subscribeToSummary: publicProcedure
|
||||
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("networkController")))
|
||||
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("networkController")))
|
||||
.subscription(({ ctx }) => {
|
||||
return observable<{
|
||||
integration: Modify<Integration, { kind: IntegrationKindByCategory<"networkController"> }>;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { releasesRequestHandler } from "@homarr/request-handler/releases";
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
const formatVersionFilterRegex = (versionFilter: z.infer<typeof _releaseVersionFilterSchema> | undefined) => {
|
||||
const formatVersionFilterRegex = (versionFilter: z.infer<typeof releaseVersionFilterSchema> | undefined) => {
|
||||
if (!versionFilter) return undefined;
|
||||
|
||||
const escapedPrefix = versionFilter.prefix ? escapeForRegEx(versionFilter.prefix) : "";
|
||||
@@ -15,7 +15,7 @@ const formatVersionFilterRegex = (versionFilter: z.infer<typeof _releaseVersionF
|
||||
return `^${escapedPrefix}${precision}${escapedSuffix}$`;
|
||||
};
|
||||
|
||||
const _releaseVersionFilterSchema = z.object({
|
||||
const releaseVersionFilterSchema = z.object({
|
||||
prefix: z.string().optional(),
|
||||
precision: z.number(),
|
||||
suffix: z.string().optional(),
|
||||
@@ -29,7 +29,7 @@ export const releasesRouter = createTRPCRouter({
|
||||
z.object({
|
||||
providerKey: z.string(),
|
||||
identifier: z.string(),
|
||||
versionFilter: _releaseVersionFilterSchema.optional(),
|
||||
versionFilter: releaseVersionFilterSchema.optional(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
|
||||
@@ -15,14 +15,14 @@ const createSmartHomeIntegrationMiddleware = (action: IntegrationAction) =>
|
||||
export const smartHomeRouter = createTRPCRouter({
|
||||
entityState: publicProcedure
|
||||
.input(z.object({ entityId: z.string() }))
|
||||
.unstable_concat(createSmartHomeIntegrationMiddleware("query"))
|
||||
.concat(createSmartHomeIntegrationMiddleware("query"))
|
||||
.query(async ({ ctx: { integration }, input }) => {
|
||||
const innerHandler = smartHomeEntityStateRequestHandler.handler(integration, { entityId: input.entityId });
|
||||
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||
return data;
|
||||
}),
|
||||
subscribeEntityState: publicProcedure
|
||||
.unstable_concat(createSmartHomeIntegrationMiddleware("query"))
|
||||
.concat(createSmartHomeIntegrationMiddleware("query"))
|
||||
.input(z.object({ entityId: z.string() }))
|
||||
.subscription(({ input, ctx }) => {
|
||||
return observable<{
|
||||
@@ -42,7 +42,7 @@ export const smartHomeRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
switchEntity: protectedProcedure
|
||||
.unstable_concat(createSmartHomeIntegrationMiddleware("interact"))
|
||||
.concat(createSmartHomeIntegrationMiddleware("interact"))
|
||||
.input(z.object({ entityId: z.string() }))
|
||||
.mutation(async ({ ctx: { integration }, input }) => {
|
||||
const client = await createIntegrationAsync(integration);
|
||||
@@ -54,7 +54,7 @@ export const smartHomeRouter = createTRPCRouter({
|
||||
return success;
|
||||
}),
|
||||
executeAutomation: protectedProcedure
|
||||
.unstable_concat(createSmartHomeIntegrationMiddleware("interact"))
|
||||
.concat(createSmartHomeIntegrationMiddleware("interact"))
|
||||
.input(z.object({ automationId: z.string() }))
|
||||
.mutation(async ({ ctx: { integration }, input }) => {
|
||||
const client = await createIntegrationAsync(integration);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { cache } from "react";
|
||||
import { headers } from "next/headers";
|
||||
import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query";
|
||||
|
||||
import { createCaller, createTRPCContext } from "@homarr/api";
|
||||
import { appRouter, createCaller, createTRPCContext } from "@homarr/api";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
|
||||
import { makeQueryClient } from "./shared";
|
||||
|
||||
/**
|
||||
* This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when
|
||||
* handling a tRPC call from a React Server Component.
|
||||
@@ -19,3 +22,12 @@ const createContext = cache(async () => {
|
||||
});
|
||||
|
||||
export const api = createCaller(createContext);
|
||||
|
||||
// IMPORTANT: Create a stable getter for the query client that
|
||||
// will return the same client during the same request.
|
||||
export const getQueryClient = cache(makeQueryClient);
|
||||
export const trpc = createTRPCOptionsProxy({
|
||||
ctx: createContext,
|
||||
router: appRouter,
|
||||
queryClient: getQueryClient,
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
import { defaultShouldDehydrateQuery, QueryClient } from "@tanstack/react-query";
|
||||
|
||||
/**
|
||||
* Creates a headers callback for a given source
|
||||
* It will set the x-trpc-source header and cookies if needed
|
||||
@@ -51,3 +53,16 @@ export const trpcPath = "/api/trpc";
|
||||
export function getTrpcUrl() {
|
||||
return `${getBaseUrl()}${trpcPath}`;
|
||||
}
|
||||
|
||||
export const makeQueryClient = () => {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 5 * 1000,
|
||||
},
|
||||
dehydrate: {
|
||||
shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === "pending",
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -13,6 +13,7 @@ import { extractProfileName } from "./providers/oidc/oidc-provider";
|
||||
|
||||
export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["events"], undefined>["signIn"] => {
|
||||
return async ({ user, profile }) => {
|
||||
logger.debug(`SignIn EventHandler for user: ${JSON.stringify(user)} . profile: ${JSON.stringify(profile)}`);
|
||||
if (!user.id) throw new Error("User ID is missing");
|
||||
|
||||
const dbUser = await db.query.users.findFirst({
|
||||
@@ -28,11 +29,13 @@ export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["
|
||||
const groupsKey = env.AUTH_OIDC_GROUPS_ATTRIBUTE;
|
||||
// Groups from oidc provider are provided from the profile, it's not typed.
|
||||
if (profile && groupsKey in profile && Array.isArray(profile[groupsKey])) {
|
||||
logger.debug(`Using profile groups (${groupsKey}): ${JSON.stringify(profile[groupsKey])}`);
|
||||
await synchronizeGroupsWithExternalForUserAsync(db, user.id, profile[groupsKey] as string[]);
|
||||
}
|
||||
|
||||
// In ldap-authroization we return the groups from ldap, it's not typed.
|
||||
if ("groups" in user && Array.isArray(user.groups)) {
|
||||
logger.debug(`Using profile groups: ${JSON.stringify(user.groups)}`);
|
||||
await synchronizeGroupsWithExternalForUserAsync(db, user.id, user.groups as string[]);
|
||||
}
|
||||
await addUserToEveryoneGroupIfNotMemberAsync(db, user.id);
|
||||
|
||||
@@ -34,13 +34,12 @@
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cookies": "^0.9.1",
|
||||
"ldapts": "7.4.0",
|
||||
"next": "15.3.1",
|
||||
"ldapts": "8.0.0",
|
||||
"next": "15.3.2",
|
||||
"next-auth": "5.0.0-beta.27",
|
||||
"pretty-print-error": "^1.1.2",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"zod": "^3.24.3"
|
||||
"zod": "^3.24.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
@@ -48,7 +47,7 @@
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/bcrypt": "5.0.2",
|
||||
"@types/cookies": "0.9.0",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
|
||||
@@ -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.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -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.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -30,17 +30,17 @@
|
||||
"@homarr/env": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"next": "15.3.1",
|
||||
"next": "15.3.2",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"undici": "7.8.0",
|
||||
"zod": "^3.24.3"
|
||||
"zod": "^3.24.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -5,3 +5,11 @@ export const splitToNChunks = <T>(array: T[], chunks: number): T[][] => {
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const splitToChunksWithNItems = <T>(array: T[], itemCount: number): T[][] => {
|
||||
const result: T[][] = [];
|
||||
for (let i = 0; i < array.length; i += itemCount) {
|
||||
result.push(array.slice(i, i + itemCount));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -1,22 +1,8 @@
|
||||
import type { CookieSerializeOptions } from "cookie";
|
||||
import { serialize } from "cookie";
|
||||
import { parse, serialize } from "cookie";
|
||||
|
||||
export function parseCookies(cookieString: string) {
|
||||
const list: Record<string, string> = {};
|
||||
const cookieHeader = cookieString;
|
||||
if (!cookieHeader) return list;
|
||||
|
||||
cookieHeader.split(";").forEach(function (cookie) {
|
||||
const items = cookie.split("=");
|
||||
let name = items.shift();
|
||||
name = name?.trim();
|
||||
if (!name) return;
|
||||
const value = items.join("=").trim();
|
||||
if (!value) return;
|
||||
list[name] = decodeURIComponent(value);
|
||||
});
|
||||
|
||||
return list;
|
||||
return parse(cookieString);
|
||||
}
|
||||
|
||||
export function setClientCookie(name: string, value: string, options: CookieSerializeOptions = {}) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import dayjs from "dayjs";
|
||||
import type { UnitTypeShort } from "dayjs";
|
||||
import isBetween from "dayjs/plugin/isBetween";
|
||||
|
||||
dayjs.extend(isBetween);
|
||||
|
||||
const validUnits = ["h", "d", "w", "M", "y"] as UnitTypeShort[];
|
||||
|
||||
export const isDateWithin = (date: Date, relativeDate: string): boolean => {
|
||||
if (relativeDate.length < 2) {
|
||||
throw new Error("Relative date must be at least 2 characters long");
|
||||
}
|
||||
|
||||
const amount = parseInt(relativeDate.slice(0, -1), 10);
|
||||
if (isNaN(amount) || amount <= 0) {
|
||||
throw new Error("Relative date must be a number greater than 0");
|
||||
}
|
||||
|
||||
const unit = relativeDate.slice(-1) as dayjs.UnitTypeShort;
|
||||
if (!validUnits.includes(unit)) {
|
||||
throw new Error("Invalid relative time unit");
|
||||
}
|
||||
|
||||
const startDate = dayjs().subtract(amount, unit);
|
||||
return dayjs(date).isBetween(startDate, dayjs(), null, "[]");
|
||||
};
|
||||
@@ -2,6 +2,7 @@ export * from "./object";
|
||||
export * from "./string";
|
||||
export * from "./cookie";
|
||||
export * from "./array";
|
||||
export * from "./date";
|
||||
export * from "./stopwatch";
|
||||
export * from "./hooks";
|
||||
export * from "./url";
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { splitToChunksWithNItems, splitToNChunks } from "../array";
|
||||
|
||||
describe("splitToNChunks", () => {
|
||||
it("should split an array into the specified number of chunks", () => {
|
||||
const array = [1, 2, 3, 4, 5];
|
||||
const chunks = 3;
|
||||
const result = splitToNChunks(array, chunks);
|
||||
expect(result).toEqual([[1, 2], [3, 4], [5]]);
|
||||
});
|
||||
|
||||
it("should handle an empty array", () => {
|
||||
const array: number[] = [];
|
||||
const chunks = 3;
|
||||
const result = splitToNChunks(array, chunks);
|
||||
expect(result).toEqual([[], [], []]);
|
||||
});
|
||||
|
||||
it("should handle more chunks than elements", () => {
|
||||
const array = [1, 2];
|
||||
const chunks = 5;
|
||||
const result = splitToNChunks(array, chunks);
|
||||
expect(result).toEqual([[1], [2], [], [], []]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("splitToChunksWithNItems", () => {
|
||||
it("should split an array into chunks with the specified number of items", () => {
|
||||
const array = [1, 2, 3, 4, 5];
|
||||
const items = 2;
|
||||
const result = splitToChunksWithNItems(array, items);
|
||||
expect(result).toEqual([[1, 2], [3, 4], [5]]);
|
||||
});
|
||||
|
||||
it("should handle an empty array", () => {
|
||||
const array: number[] = [];
|
||||
const items = 2;
|
||||
const result = splitToChunksWithNItems(array, items);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should handle more items per chunk than elements", () => {
|
||||
const array = [1, 2];
|
||||
const items = 5;
|
||||
const result = splitToChunksWithNItems(array, items);
|
||||
expect(result).toEqual([[1, 2]]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { isDateWithin } from "../date";
|
||||
|
||||
describe("isDateWithin", () => {
|
||||
it("should return true for a date within the specified hours", () => {
|
||||
const date = new Date();
|
||||
date.setHours(date.getHours() - 20);
|
||||
expect(isDateWithin(date, "100h")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for a date outside the specified hours", () => {
|
||||
const date = new Date();
|
||||
date.setHours(date.getHours() - 101);
|
||||
expect(isDateWithin(date, "100h")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for a date within the specified days", () => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - 5);
|
||||
expect(isDateWithin(date, "10d")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for a date outside the specified days", () => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - 11);
|
||||
expect(isDateWithin(date, "10d")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for a date within the specified weeks", () => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - 10);
|
||||
expect(isDateWithin(date, "7w")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for a date outside the specified weeks", () => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - 50);
|
||||
expect(isDateWithin(date, "7w")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for a date within the specified months", () => {
|
||||
const date = new Date();
|
||||
date.setMonth(date.getMonth() - 1);
|
||||
expect(isDateWithin(date, "2M")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for a date outside the specified months", () => {
|
||||
const date = new Date();
|
||||
date.setMonth(date.getMonth() - 3);
|
||||
expect(isDateWithin(date, "2M")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for a date within the specified years", () => {
|
||||
const date = new Date();
|
||||
date.setFullYear(date.getFullYear() - 1);
|
||||
expect(isDateWithin(date, "2y")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for a date outside the specified years", () => {
|
||||
const date = new Date();
|
||||
date.setFullYear(date.getFullYear() - 3);
|
||||
expect(isDateWithin(date, "2y")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for a date after the specified relative time", () => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + 2);
|
||||
expect(isDateWithin(date, "1d")).toBe(false);
|
||||
});
|
||||
|
||||
it("should throw an error for an invalid unit", () => {
|
||||
const date = new Date();
|
||||
expect(() => isDateWithin(date, "2x")).toThrow("Invalid relative time unit");
|
||||
});
|
||||
|
||||
it("should throw an error if relativeDate is less than 2 characters long", () => {
|
||||
const date = new Date();
|
||||
expect(() => isDateWithin(date, "h")).toThrow("Relative date must be at least 2 characters long");
|
||||
});
|
||||
|
||||
it("should throw an error if relativeDate has an invalid number", () => {
|
||||
const date = new Date();
|
||||
expect(() => isDateWithin(date, "hh")).toThrow("Relative date must be a number greater than 0");
|
||||
});
|
||||
|
||||
it("should throw an error if relativeDate is set to 0", () => {
|
||||
const date = new Date();
|
||||
expect(() => isDateWithin(date, "0y")).toThrow("Relative date must be a number greater than 0");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { getPortFromUrl } from "../url";
|
||||
|
||||
describe("getPortFromUrl", () => {
|
||||
test.each([
|
||||
[80, "http"],
|
||||
[443, "https"],
|
||||
])("should return %s for %s protocol without port", (expectedPort, protocol) => {
|
||||
// Arrange
|
||||
const url = new URL(`${protocol}://example.com`);
|
||||
|
||||
// Act
|
||||
const port = getPortFromUrl(url);
|
||||
|
||||
// Assert
|
||||
expect(port).toBe(expectedPort);
|
||||
});
|
||||
test.each([["http"], ["https"], ["anything"]])("should return the specified port for %s protocol", (protocol) => {
|
||||
// Arrange
|
||||
const expectedPort = 3000;
|
||||
const url = new URL(`${protocol}://example.com:${expectedPort}`);
|
||||
|
||||
// Act
|
||||
const port = getPortFromUrl(url);
|
||||
|
||||
// Assert
|
||||
expect(port).toBe(expectedPort);
|
||||
});
|
||||
test("should throw an error for unsupported protocol", () => {
|
||||
// Arrange
|
||||
const url = new URL("ftp://example.com");
|
||||
|
||||
// Act
|
||||
const act = () => getPortFromUrl(url);
|
||||
|
||||
// Act & Assert
|
||||
expect(act).toThrowError("Unsupported protocol: ftp:");
|
||||
});
|
||||
});
|
||||
@@ -21,3 +21,20 @@ export const extractBaseUrlFromHeaders = (
|
||||
|
||||
return `${protocol}://${host}`;
|
||||
};
|
||||
|
||||
export const getPortFromUrl = (url: URL): number => {
|
||||
const port = url.port;
|
||||
if (port) {
|
||||
return Number(port);
|
||||
}
|
||||
|
||||
if (url.protocol === "https:") {
|
||||
return 443;
|
||||
}
|
||||
|
||||
if (url.protocol === "http:") {
|
||||
return 80;
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported protocol: ${url.protocol}`);
|
||||
};
|
||||
@@ -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.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -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.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -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.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -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.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -44,12 +44,12 @@
|
||||
"@homarr/env": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.17.5",
|
||||
"@mantine/core": "^8.0.0",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@testcontainers/mysql": "^10.24.2",
|
||||
"better-sqlite3": "^11.9.1",
|
||||
"@testcontainers/mysql": "^10.25.0",
|
||||
"better-sqlite3": "^11.10.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"drizzle-kit": "^0.31.0",
|
||||
"drizzle-kit": "^0.31.1",
|
||||
"drizzle-orm": "^0.43.1",
|
||||
"drizzle-zod": "^0.7.1",
|
||||
"mysql2": "3.14.1"
|
||||
@@ -60,9 +60,9 @@
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/better-sqlite3": "7.6.13",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"prettier": "^3.5.3",
|
||||
"tsx": "4.19.3",
|
||||
"tsx": "4.19.4",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -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.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -113,6 +113,8 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/tags/proxmox"
|
||||
| "/docs/tags/proxy"
|
||||
| "/docs/tags/puid"
|
||||
| "/docs/tags/releases"
|
||||
| "/docs/tags/repositories"
|
||||
| "/docs/tags/responsive"
|
||||
| "/docs/tags/roles"
|
||||
| "/docs/tags/rss"
|
||||
@@ -212,6 +214,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/widgets/media-server"
|
||||
| "/docs/widgets/minecraft-server-status"
|
||||
| "/docs/widgets/notebook"
|
||||
| "/docs/widgets/releases"
|
||||
| "/docs/widgets/rss"
|
||||
| "/docs/widgets/stocks"
|
||||
| "/docs/widgets/video"
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/dockerode": "^3.3.38",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
Vendored
+3
-3
@@ -23,14 +23,14 @@
|
||||
},
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@t3-oss/env-nextjs": "^0.13.0",
|
||||
"zod": "^3.24.3"
|
||||
"@t3-oss/env-nextjs": "^0.13.4",
|
||||
"zod": "^3.24.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -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.5",
|
||||
"zod": "^3.24.3"
|
||||
"@mantine/form": "^8.0.0",
|
||||
"zod": "^3.24.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -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.5",
|
||||
"@mantine/core": "^8.0.0",
|
||||
"react": "19.1.0",
|
||||
"zod": "^3.24.3"
|
||||
"zod": "^3.24.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -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.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@
|
||||
"tsdav": "^2.1.4",
|
||||
"undici": "7.8.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"zod": "^3.24.3"
|
||||
"zod": "^3.24.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
@@ -51,7 +51,7 @@
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node-unifi": "^2.5.1",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,7 @@ export enum MediaRequestStatus {
|
||||
Approved = 2,
|
||||
Declined = 3,
|
||||
Failed = 4,
|
||||
Completed = 5,
|
||||
}
|
||||
|
||||
export enum MediaAvailability {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { SiteStats } from "node-unifi";
|
||||
import { Controller } from "node-unifi";
|
||||
|
||||
import { getPortFromUrl } from "@homarr/common";
|
||||
|
||||
import { Integration } from "../base/integration";
|
||||
import type { NetworkControllerSummaryIntegration } from "../interfaces/network-controller-summary/network-controller-summary-integration";
|
||||
import type { NetworkControllerSummary } from "../interfaces/network-controller-summary/network-controller-summary-types";
|
||||
@@ -42,20 +44,16 @@ export class UnifiControllerIntegration extends Integration implements NetworkCo
|
||||
}
|
||||
|
||||
private async createControllerClientAsync() {
|
||||
const portString = new URL(this.integration.url).port;
|
||||
const port = Number.isInteger(portString) ? Number(portString) : undefined;
|
||||
const hostname = new URL(this.integration.url).hostname;
|
||||
const url = new URL(this.integration.url);
|
||||
|
||||
const client = new Controller({
|
||||
host: hostname,
|
||||
// @ts-expect-error the URL construction is incorrect and does not append the required / at the end: https://github.com/jens-maus/node-unifi/blob/05665e8f82a900a15a9ea8b1071750b29825b3bc/unifi.js#L56, https://github.com/jens-maus/node-unifi/blob/05665e8f82a900a15a9ea8b1071750b29825b3bc/unifi.js#L95
|
||||
port: port === undefined ? "/" : `${port}/`,
|
||||
host: url.hostname,
|
||||
port: getPortFromUrl(url),
|
||||
sslverify: false, // TODO: implement a "ignore certificate toggle", see https://github.com/homarr-labs/homarr/issues/2553
|
||||
username: this.getSecretValue("username"),
|
||||
password: this.getSecretValue("password"),
|
||||
});
|
||||
|
||||
// Object.defineProperty(client, '_baseurl', { value: url });
|
||||
await client.login(this.getSecretValue("username"), this.getSecretValue("password"), null);
|
||||
return client;
|
||||
}
|
||||
|
||||
@@ -27,13 +27,13 @@
|
||||
"ioredis": "5.6.1",
|
||||
"superjson": "2.2.2",
|
||||
"winston": "3.17.0",
|
||||
"zod": "^3.24.3"
|
||||
"zod": "^3.24.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,10 @@ export const formatErrorCause = (cause: unknown, iteration = 0): string => {
|
||||
return `\ncaused by ${formatErrorTitle(cause)}\n${formatErrorStack(cause.stack)}${formatErrorCause(cause.cause, iteration + 1)}`;
|
||||
}
|
||||
|
||||
if (cause instanceof Object) {
|
||||
return `\ncaused by ${JSON.stringify(cause)}`;
|
||||
}
|
||||
|
||||
return `\ncaused by ${cause as string}`;
|
||||
};
|
||||
|
||||
|
||||
@@ -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.5",
|
||||
"@mantine/core": "^8.0.0",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"next": "15.3.1",
|
||||
"next": "15.3.2",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"zod": "^3.24.3"
|
||||
"zod": "^3.24.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -24,15 +24,15 @@
|
||||
"dependencies": {
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.17.5",
|
||||
"@mantine/hooks": "^7.17.5",
|
||||
"@mantine/core": "^8.0.0",
|
||||
"@mantine/hooks": "^8.0.0",
|
||||
"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.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -24,14 +24,14 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@mantine/notifications": "^7.17.5",
|
||||
"@mantine/notifications": "^8.0.0",
|
||||
"@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.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -37,14 +37,14 @@
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.17.5",
|
||||
"@mantine/hooks": "^7.17.5",
|
||||
"@mantine/core": "^8.0.0",
|
||||
"@mantine/hooks": "^8.0.0",
|
||||
"adm-zip": "0.5.16",
|
||||
"next": "15.3.1",
|
||||
"next": "15.3.2",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"superjson": "2.2.2",
|
||||
"zod": "^3.24.3",
|
||||
"zod": "^3.24.4",
|
||||
"zod-form-data": "^2.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -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.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -23,13 +23,13 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"zod": "^3.24.3"
|
||||
"zod": "^3.24.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -24,14 +24,13 @@
|
||||
"dependencies": {
|
||||
"@homarr/certificates": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"pretty-print-error": "^1.1.2"
|
||||
"@homarr/log": "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.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -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.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@extractus/feed-extractor": "7.1.4",
|
||||
"@extractus/feed-extractor": "7.1.5",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/definitions": "workspace:^0.1.0",
|
||||
@@ -31,14 +31,13 @@
|
||||
"@homarr/redis": "workspace:^0.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"octokit": "^4.1.3",
|
||||
"pretty-print-error": "^1.1.2",
|
||||
"superjson": "2.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -41,12 +41,8 @@ export const Providers: ProvidersProps = {
|
||||
.transform((resp) => ({
|
||||
projectUrl: `https://hub.docker.com/r/${resp.namespace === "library" ? "_" : resp.namespace}/${resp.name}`,
|
||||
projectDescription: resp.description,
|
||||
isFork: false,
|
||||
isArchived: false,
|
||||
createdAt: resp.date_registered,
|
||||
starsCount: resp.star_count,
|
||||
openIssues: 0,
|
||||
forksCount: 0,
|
||||
}))
|
||||
.safeParse(response);
|
||||
},
|
||||
@@ -67,12 +63,7 @@ export const Providers: ProvidersProps = {
|
||||
),
|
||||
})
|
||||
.transform((resp) => {
|
||||
return resp.results.map((release) => ({
|
||||
...release,
|
||||
releaseUrl: "",
|
||||
releaseDescription: "",
|
||||
isPreRelease: false,
|
||||
}));
|
||||
return resp.results;
|
||||
})
|
||||
.safeParse(response);
|
||||
},
|
||||
@@ -89,7 +80,7 @@ export const Providers: ProvidersProps = {
|
||||
return z
|
||||
.object({
|
||||
html_url: z.string(),
|
||||
description: z.string(),
|
||||
description: z.string().nullable(),
|
||||
fork: z.boolean(),
|
||||
archived: z.boolean(),
|
||||
created_at: z.string().transform((value) => new Date(value)),
|
||||
@@ -99,7 +90,7 @@ export const Providers: ProvidersProps = {
|
||||
})
|
||||
.transform((resp) => ({
|
||||
projectUrl: resp.html_url,
|
||||
projectDescription: resp.description,
|
||||
projectDescription: resp.description ?? undefined,
|
||||
isFork: resp.fork,
|
||||
isArchived: resp.archived,
|
||||
createdAt: resp.created_at,
|
||||
@@ -120,7 +111,7 @@ export const Providers: ProvidersProps = {
|
||||
tag_name: z.string(),
|
||||
published_at: z.string().transform((value) => new Date(value)),
|
||||
html_url: z.string(),
|
||||
body: z.string(),
|
||||
body: z.string().nullable(),
|
||||
prerelease: z.boolean(),
|
||||
})
|
||||
.transform((tag) => ({
|
||||
@@ -128,7 +119,7 @@ export const Providers: ProvidersProps = {
|
||||
latestRelease: tag.tag_name,
|
||||
latestReleaseAt: tag.published_at,
|
||||
releaseUrl: tag.html_url,
|
||||
releaseDescription: tag.body,
|
||||
releaseDescription: tag.body ?? undefined,
|
||||
isPreRelease: tag.prerelease,
|
||||
})),
|
||||
)
|
||||
@@ -144,17 +135,17 @@ export const Providers: ProvidersProps = {
|
||||
.object({
|
||||
web_url: z.string(),
|
||||
description: z.string(),
|
||||
forked_from_project: z.object({ id: z.number() }).nullable(),
|
||||
archived: z.boolean(),
|
||||
forked_from_project: z.object({ id: z.number() }).optional(),
|
||||
archived: z.boolean().optional(),
|
||||
created_at: z.string().transform((value) => new Date(value)),
|
||||
star_count: z.number(),
|
||||
open_issues_count: z.number(),
|
||||
open_issues_count: z.number().optional(),
|
||||
forks_count: z.number(),
|
||||
})
|
||||
.transform((resp) => ({
|
||||
projectUrl: resp.web_url,
|
||||
projectDescription: resp.description,
|
||||
isFork: resp.forked_from_project !== null,
|
||||
isFork: resp.forked_from_project !== undefined,
|
||||
isArchived: resp.archived,
|
||||
createdAt: resp.created_at,
|
||||
starsCount: resp.star_count,
|
||||
@@ -217,7 +208,6 @@ export const Providers: ProvidersProps = {
|
||||
...release,
|
||||
releaseUrl: `https://www.npmjs.com/package/${resp.name}/v/${release.latestRelease}`,
|
||||
releaseDescription: resp.versions[release.latestRelease]?.description ?? "",
|
||||
isPreRelease: false,
|
||||
}));
|
||||
})
|
||||
.safeParse(response);
|
||||
@@ -282,23 +272,31 @@ export const Providers: ProvidersProps = {
|
||||
},
|
||||
};
|
||||
|
||||
const _detailsSchema = z.object({
|
||||
projectUrl: z.string(),
|
||||
projectDescription: z.string(),
|
||||
isFork: z.boolean(),
|
||||
isArchived: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
starsCount: z.number(),
|
||||
openIssues: z.number(),
|
||||
forksCount: z.number(),
|
||||
});
|
||||
const _detailsSchema = z
|
||||
.object({
|
||||
projectUrl: z.string().optional(),
|
||||
projectDescription: z.string().optional(),
|
||||
isFork: z.boolean().optional(),
|
||||
isArchived: z.boolean().optional(),
|
||||
createdAt: z.date().optional(),
|
||||
starsCount: z.number().optional(),
|
||||
openIssues: z.number().optional(),
|
||||
forksCount: z.number().optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
const _releasesSchema = z.object({
|
||||
latestRelease: z.string(),
|
||||
latestReleaseAt: z.date(),
|
||||
releaseUrl: z.string(),
|
||||
releaseDescription: z.string(),
|
||||
isPreRelease: z.boolean(),
|
||||
releaseUrl: z.string().optional(),
|
||||
releaseDescription: z.string().optional(),
|
||||
isPreRelease: z.boolean().optional(),
|
||||
error: z
|
||||
.object({
|
||||
code: z.string().optional(),
|
||||
message: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type DetailsResponse = z.infer<typeof _detailsSchema>;
|
||||
|
||||
@@ -8,22 +8,49 @@ import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-ha
|
||||
import { Providers } from "./releases-providers";
|
||||
import type { DetailsResponse } from "./releases-providers";
|
||||
|
||||
const errorSchema = z.object({
|
||||
code: z.string().optional(),
|
||||
message: z.string().optional(),
|
||||
});
|
||||
|
||||
type ReleasesError = z.infer<typeof errorSchema>;
|
||||
|
||||
const _reponseSchema = z.object({
|
||||
identifier: z.string(),
|
||||
providerKey: z.string(),
|
||||
latestRelease: z.string(),
|
||||
latestReleaseAt: z.date(),
|
||||
releaseUrl: z.string(),
|
||||
releaseDescription: z.string(),
|
||||
isPreRelease: z.boolean(),
|
||||
projectUrl: z.string(),
|
||||
projectDescription: z.string(),
|
||||
isFork: z.boolean(),
|
||||
isArchived: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
starsCount: z.number(),
|
||||
openIssues: z.number(),
|
||||
forksCount: z.number(),
|
||||
latestRelease: z.string().optional(),
|
||||
latestReleaseAt: z.date().optional(),
|
||||
releaseUrl: z.string().optional(),
|
||||
releaseDescription: z.string().optional(),
|
||||
isPreRelease: z.boolean().optional(),
|
||||
projectUrl: z.string().optional(),
|
||||
projectDescription: z.string().optional(),
|
||||
isFork: z.boolean().optional(),
|
||||
isArchived: z.boolean().optional(),
|
||||
createdAt: z.date().optional(),
|
||||
starsCount: z.number().optional(),
|
||||
openIssues: z.number().optional(),
|
||||
forksCount: z.number().optional(),
|
||||
error: errorSchema.optional(),
|
||||
});
|
||||
|
||||
const formatErrorRelease = (identifier: string, providerKey: string, error: ReleasesError) => ({
|
||||
identifier,
|
||||
providerKey,
|
||||
latestRelease: undefined,
|
||||
latestReleaseAt: undefined,
|
||||
releaseUrl: undefined,
|
||||
releaseDescription: undefined,
|
||||
isPreRelease: undefined,
|
||||
projectUrl: undefined,
|
||||
projectDescription: undefined,
|
||||
isFork: undefined,
|
||||
isArchived: undefined,
|
||||
createdAt: undefined,
|
||||
starsCount: undefined,
|
||||
openIssues: undefined,
|
||||
forksCount: undefined,
|
||||
error,
|
||||
});
|
||||
|
||||
export const releasesRequestHandler = createCachedWidgetRequestHandler({
|
||||
@@ -34,17 +61,7 @@ export const releasesRequestHandler = createCachedWidgetRequestHandler({
|
||||
|
||||
if (!provider) return undefined;
|
||||
|
||||
let detailsResult: DetailsResponse = {
|
||||
projectUrl: "",
|
||||
projectDescription: "",
|
||||
isFork: false,
|
||||
isArchived: false,
|
||||
createdAt: new Date(0),
|
||||
starsCount: 0,
|
||||
openIssues: 0,
|
||||
forksCount: 0,
|
||||
};
|
||||
|
||||
let detailsResult: DetailsResponse;
|
||||
const detailsUrl = provider.getDetailsUrl(input.identifier);
|
||||
if (detailsUrl !== undefined) {
|
||||
const detailsResponse = await fetchWithTimeout(detailsUrl);
|
||||
@@ -53,7 +70,8 @@ export const releasesRequestHandler = createCachedWidgetRequestHandler({
|
||||
if (parsedDetails?.success) {
|
||||
detailsResult = parsedDetails.data;
|
||||
} else {
|
||||
logger.warn("Failed to parse details response", {
|
||||
detailsResult = undefined;
|
||||
logger.warn(`Failed to parse details response for ${input.identifier} on ${input.providerKey}`, {
|
||||
provider: input.providerKey,
|
||||
identifier: input.identifier,
|
||||
detailsUrl,
|
||||
@@ -63,43 +81,42 @@ export const releasesRequestHandler = createCachedWidgetRequestHandler({
|
||||
}
|
||||
|
||||
const releasesResponse = await fetchWithTimeout(provider.getReleasesUrl(input.identifier));
|
||||
const releasesResult = provider.parseReleasesResponse(await releasesResponse.json());
|
||||
const releasesResponseJson: unknown = await releasesResponse.json();
|
||||
const releasesResult = provider.parseReleasesResponse(releasesResponseJson);
|
||||
|
||||
if (!releasesResult.success) return undefined;
|
||||
|
||||
const latest: ResponseResponse = releasesResult.data
|
||||
.filter((result) => (input.versionRegex ? new RegExp(input.versionRegex).test(result.latestRelease) : true))
|
||||
.reduce(
|
||||
(latest, result) => {
|
||||
return {
|
||||
...detailsResult,
|
||||
...(result.latestReleaseAt > latest.latestReleaseAt ? result : latest),
|
||||
identifier: input.identifier,
|
||||
providerKey: input.providerKey,
|
||||
};
|
||||
},
|
||||
{
|
||||
identifier: "",
|
||||
providerKey: "",
|
||||
latestRelease: "",
|
||||
latestReleaseAt: new Date(0),
|
||||
releaseUrl: "",
|
||||
releaseDescription: "",
|
||||
isPreRelease: false,
|
||||
projectUrl: "",
|
||||
projectDescription: "",
|
||||
isFork: false,
|
||||
isArchived: false,
|
||||
createdAt: new Date(0),
|
||||
starsCount: 0,
|
||||
openIssues: 0,
|
||||
forksCount: 0,
|
||||
},
|
||||
if (!releasesResult.success) {
|
||||
return formatErrorRelease(input.identifier, input.providerKey, {
|
||||
message: releasesResponseJson ? JSON.stringify(releasesResponseJson, null, 2) : releasesResult.error.message,
|
||||
});
|
||||
} else {
|
||||
const releases = releasesResult.data.filter((result) =>
|
||||
input.versionRegex && result.latestRelease ? new RegExp(input.versionRegex).test(result.latestRelease) : true,
|
||||
);
|
||||
|
||||
return latest;
|
||||
const latest =
|
||||
releases.length === 0
|
||||
? formatErrorRelease(input.identifier, input.providerKey, { code: "noMatchingVersion" })
|
||||
: releases.reduce(
|
||||
(latest, result) => {
|
||||
return {
|
||||
...detailsResult,
|
||||
...(result.latestReleaseAt > latest.latestReleaseAt ? result : latest),
|
||||
identifier: input.identifier,
|
||||
providerKey: input.providerKey,
|
||||
};
|
||||
},
|
||||
{
|
||||
identifier: "",
|
||||
providerKey: "",
|
||||
latestRelease: "",
|
||||
latestReleaseAt: new Date(0),
|
||||
},
|
||||
);
|
||||
|
||||
return latest;
|
||||
}
|
||||
},
|
||||
cacheDuration: dayjs.duration(5, "minutes"),
|
||||
});
|
||||
|
||||
export type ResponseResponse = z.infer<typeof _reponseSchema>;
|
||||
export type ReleaseResponse = z.infer<typeof _reponseSchema>;
|
||||
@@ -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.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./index.ts"
|
||||
".": "./index.ts",
|
||||
"./creator": "./src/creator.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
@@ -25,8 +26,8 @@
|
||||
"@homarr/api": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@mantine/dates": "^7.17.5",
|
||||
"next": "15.3.1",
|
||||
"@mantine/dates": "^8.0.0",
|
||||
"next": "15.3.2",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0"
|
||||
},
|
||||
@@ -34,7 +35,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -2,30 +2,9 @@
|
||||
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { createContext, useContext } from "react";
|
||||
import type { DayOfWeek } from "@mantine/dates";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import type { User } from "@homarr/db/schema";
|
||||
import type { ServerSettings } from "@homarr/server-settings";
|
||||
|
||||
export type SettingsContextProps = Pick<
|
||||
User,
|
||||
| "firstDayOfWeek"
|
||||
| "defaultSearchEngineId"
|
||||
| "homeBoardId"
|
||||
| "mobileHomeBoardId"
|
||||
| "openSearchInNewTab"
|
||||
| "pingIconsEnabled"
|
||||
> &
|
||||
Pick<ServerSettings["board"], "enableStatusByDefault" | "forceDisableStatus">;
|
||||
|
||||
interface PublicServerSettings {
|
||||
search: Pick<ServerSettings["search"], "defaultSearchEngineId">;
|
||||
board: Pick<
|
||||
ServerSettings["board"],
|
||||
"homeBoardId" | "mobileHomeBoardId" | "enableStatusByDefault" | "forceDisableStatus"
|
||||
>;
|
||||
}
|
||||
import type { PublicServerSettings, SettingsContextProps, UserSettings } from "./creator";
|
||||
import { createSettings } from "./creator";
|
||||
|
||||
const SettingsContext = createContext<SettingsContextProps | null>(null);
|
||||
|
||||
@@ -33,22 +12,9 @@ export const SettingsProvider = ({
|
||||
user,
|
||||
serverSettings,
|
||||
children,
|
||||
}: PropsWithChildren<{ user: RouterOutputs["user"]["getById"] | null; serverSettings: PublicServerSettings }>) => {
|
||||
}: PropsWithChildren<{ user: UserSettings | null; serverSettings: PublicServerSettings }>) => {
|
||||
return (
|
||||
<SettingsContext.Provider
|
||||
value={{
|
||||
defaultSearchEngineId: user?.defaultSearchEngineId ?? serverSettings.search.defaultSearchEngineId,
|
||||
openSearchInNewTab: user?.openSearchInNewTab ?? true,
|
||||
firstDayOfWeek: (user?.firstDayOfWeek as DayOfWeek | undefined) ?? (1 as const),
|
||||
homeBoardId: user?.homeBoardId ?? serverSettings.board.homeBoardId,
|
||||
mobileHomeBoardId: user?.mobileHomeBoardId ?? serverSettings.board.mobileHomeBoardId,
|
||||
pingIconsEnabled: user?.pingIconsEnabled ?? false,
|
||||
enableStatusByDefault: serverSettings.board.enableStatusByDefault,
|
||||
forceDisableStatus: serverSettings.board.forceDisableStatus,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SettingsContext.Provider>
|
||||
<SettingsContext.Provider value={createSettings({ user, serverSettings })}>{children}</SettingsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { User } from "@homarr/db/schema";
|
||||
import type { ServerSettings } from "@homarr/server-settings";
|
||||
|
||||
export type SettingsContextProps = Pick<
|
||||
User,
|
||||
| "firstDayOfWeek"
|
||||
| "defaultSearchEngineId"
|
||||
| "homeBoardId"
|
||||
| "mobileHomeBoardId"
|
||||
| "openSearchInNewTab"
|
||||
| "pingIconsEnabled"
|
||||
> &
|
||||
Pick<ServerSettings["board"], "enableStatusByDefault" | "forceDisableStatus">;
|
||||
|
||||
export interface PublicServerSettings {
|
||||
search: Pick<ServerSettings["search"], "defaultSearchEngineId">;
|
||||
board: Pick<
|
||||
ServerSettings["board"],
|
||||
"homeBoardId" | "mobileHomeBoardId" | "enableStatusByDefault" | "forceDisableStatus"
|
||||
>;
|
||||
}
|
||||
|
||||
export type UserSettings = Pick<
|
||||
User,
|
||||
| "firstDayOfWeek"
|
||||
| "defaultSearchEngineId"
|
||||
| "homeBoardId"
|
||||
| "mobileHomeBoardId"
|
||||
| "openSearchInNewTab"
|
||||
| "pingIconsEnabled"
|
||||
>;
|
||||
|
||||
export const createSettings = ({
|
||||
user,
|
||||
serverSettings,
|
||||
}: {
|
||||
user: UserSettings | null;
|
||||
serverSettings: PublicServerSettings;
|
||||
}) => ({
|
||||
defaultSearchEngineId: user?.defaultSearchEngineId ?? serverSettings.search.defaultSearchEngineId,
|
||||
openSearchInNewTab: user?.openSearchInNewTab ?? true,
|
||||
firstDayOfWeek: user?.firstDayOfWeek ?? (1 as const),
|
||||
homeBoardId: user?.homeBoardId ?? serverSettings.board.homeBoardId,
|
||||
mobileHomeBoardId: user?.mobileHomeBoardId ?? serverSettings.board.mobileHomeBoardId,
|
||||
pingIconsEnabled: user?.pingIconsEnabled ?? false,
|
||||
enableStatusByDefault: serverSettings.board.enableStatusByDefault,
|
||||
forceDisableStatus: serverSettings.board.forceDisableStatus,
|
||||
});
|
||||
@@ -33,12 +33,12 @@
|
||||
"@homarr/settings": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.17.5",
|
||||
"@mantine/hooks": "^7.17.5",
|
||||
"@mantine/spotlight": "^7.17.5",
|
||||
"@mantine/core": "^8.0.0",
|
||||
"@mantine/hooks": "^8.0.0",
|
||||
"@mantine/spotlight": "^8.0.0",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"jotai": "^2.12.3",
|
||||
"next": "15.3.1",
|
||||
"jotai": "^2.12.4",
|
||||
"next": "15.3.2",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"use-deep-compare-effect": "^1.8.1"
|
||||
@@ -47,7 +47,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"deepmerge": "4.3.1",
|
||||
"mantine-react-table": "2.0.0-beta.9",
|
||||
"next": "15.3.1",
|
||||
"next": "15.3.2",
|
||||
"next-intl": "4.1.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0"
|
||||
@@ -41,7 +41,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -1949,7 +1949,8 @@
|
||||
"pending": "",
|
||||
"approved": "",
|
||||
"declined": "",
|
||||
"failed": ""
|
||||
"failed": "",
|
||||
"completed": ""
|
||||
},
|
||||
"toBeDetermined": ""
|
||||
},
|
||||
|
||||
@@ -1949,7 +1949,8 @@
|
||||
"pending": "待处理",
|
||||
"approved": "已批准",
|
||||
"declined": "已拒绝",
|
||||
"failed": "失败"
|
||||
"failed": "失败",
|
||||
"completed": ""
|
||||
},
|
||||
"toBeDetermined": "待定"
|
||||
},
|
||||
|
||||
@@ -1949,7 +1949,8 @@
|
||||
"pending": "",
|
||||
"approved": "",
|
||||
"declined": "",
|
||||
"failed": ""
|
||||
"failed": "",
|
||||
"completed": ""
|
||||
},
|
||||
"toBeDetermined": ""
|
||||
},
|
||||
|
||||
@@ -1949,7 +1949,8 @@
|
||||
"pending": "Afventende",
|
||||
"approved": "Godkendt",
|
||||
"declined": "Afvist",
|
||||
"failed": "Mislykket"
|
||||
"failed": "Mislykket",
|
||||
"completed": ""
|
||||
},
|
||||
"toBeDetermined": "TBD"
|
||||
},
|
||||
|
||||
@@ -1949,7 +1949,8 @@
|
||||
"pending": "Ausstehend",
|
||||
"approved": "Bestätigt",
|
||||
"declined": "Abgelehnt",
|
||||
"failed": "Fehlgeschlagen"
|
||||
"failed": "Fehlgeschlagen",
|
||||
"completed": ""
|
||||
},
|
||||
"toBeDetermined": "Noch Festzulegen"
|
||||
},
|
||||
|
||||
@@ -618,11 +618,11 @@
|
||||
"title": ""
|
||||
},
|
||||
"create": {
|
||||
"title": "",
|
||||
"title": "Neue App erstellen",
|
||||
"description": "",
|
||||
"action": ""
|
||||
},
|
||||
"add": ""
|
||||
"add": "App hinzufügen"
|
||||
}
|
||||
},
|
||||
"integration": {
|
||||
@@ -995,7 +995,7 @@
|
||||
},
|
||||
"option": {
|
||||
"title": {
|
||||
"label": ""
|
||||
"label": "Titel"
|
||||
},
|
||||
"borderColor": {
|
||||
"label": "Rahmenfarbe"
|
||||
@@ -1949,7 +1949,8 @@
|
||||
"pending": "Ausstehend",
|
||||
"approved": "Bestätigt",
|
||||
"declined": "Abgelehnt",
|
||||
"failed": "Fehlgeschlagen"
|
||||
"failed": "Fehlgeschlagen",
|
||||
"completed": ""
|
||||
},
|
||||
"toBeDetermined": "Noch Festzulegen"
|
||||
},
|
||||
|
||||
@@ -1949,7 +1949,8 @@
|
||||
"pending": "",
|
||||
"approved": "",
|
||||
"declined": "",
|
||||
"failed": ""
|
||||
"failed": "",
|
||||
"completed": ""
|
||||
},
|
||||
"toBeDetermined": ""
|
||||
},
|
||||
|
||||
@@ -1949,7 +1949,8 @@
|
||||
"pending": "",
|
||||
"approved": "",
|
||||
"declined": "",
|
||||
"failed": ""
|
||||
"failed": "",
|
||||
"completed": ""
|
||||
},
|
||||
"toBeDetermined": ""
|
||||
},
|
||||
|
||||
@@ -1949,7 +1949,8 @@
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"declined": "Declined",
|
||||
"failed": "Failed"
|
||||
"failed": "Failed",
|
||||
"completed": "Completed"
|
||||
},
|
||||
"toBeDetermined": "TBD"
|
||||
},
|
||||
@@ -2058,11 +2059,11 @@
|
||||
"option": {
|
||||
"newReleaseWithin": {
|
||||
"label": "New Release Within",
|
||||
"description": "Usage example: 1w (1 week), 10m (10 months). Accepted unit types h (hours), d (days), w (weeks), m (months), y (years). Leave empty for no highlighting of new releases."
|
||||
"description": "Usage example: 1w (1 week), 10M (10 months). Accepted unit types h (hours), d (days), w (weeks), M (months), y (years). Leave empty for no highlighting of new releases."
|
||||
},
|
||||
"staleReleaseWithin": {
|
||||
"label": "Stale Release Within",
|
||||
"description": "Usage example: 1w (1 week), 10m (10 months). Accepted unit types h (hours), d (days), w (weeks), m (months), y (years). Leave empty for no highlighting of stale releases."
|
||||
"description": "Usage example: 1w (1 week), 10M (10 months). Accepted unit types h (hours), d (days), w (weeks), M (months), y (years). Leave empty for no highlighting of stale releases."
|
||||
},
|
||||
"showOnlyHighlighted": {
|
||||
"label": "Show Only Highlighted",
|
||||
@@ -2129,7 +2130,13 @@
|
||||
"openProjectPage": "Open Project Page",
|
||||
"openReleasePage": "Open Release Page",
|
||||
"releaseDescription": "Release Description",
|
||||
"created": "Created"
|
||||
"created": "Created",
|
||||
"error": {
|
||||
"label": "Error",
|
||||
"options": {
|
||||
"noMatchingVersion": "No matching version found"
|
||||
}
|
||||
}
|
||||
},
|
||||
"networkControllerSummary": {
|
||||
"option": {},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"step": {
|
||||
"start": {
|
||||
"title": "Bienvenido a Homarr",
|
||||
"subtitle": "",
|
||||
"subtitle": "Comencemos con la configuración de tu instancia de Homar.",
|
||||
"description": "Para empezar, por favor, seleccione cómo desea configurar su instancia de Homarr.",
|
||||
"action": {
|
||||
"scratch": "Empezar de cero",
|
||||
@@ -27,18 +27,18 @@
|
||||
"description": ""
|
||||
},
|
||||
"boardSelection": {
|
||||
"title": "",
|
||||
"title": "Se encontraron {count} tableros",
|
||||
"description": "",
|
||||
"action": {
|
||||
"selectAll": "",
|
||||
"unselectAll": ""
|
||||
"selectAll": "Seleccionar todo",
|
||||
"unselectAll": "Deseleccionar todo"
|
||||
}
|
||||
},
|
||||
"summary": {
|
||||
"title": "Importar resumen",
|
||||
"description": "",
|
||||
"action": {
|
||||
"import": ""
|
||||
"import": "Confirmar la importación y continuar"
|
||||
},
|
||||
"entities": {
|
||||
"apps": "Aplicaciones",
|
||||
@@ -78,24 +78,24 @@
|
||||
},
|
||||
"group": {
|
||||
"title": "",
|
||||
"subtitle": "",
|
||||
"subtitle": "Especifique el grupo que debe ser usado para usuarios externos.",
|
||||
"form": {
|
||||
"name": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Nombre del grupo",
|
||||
"description": "El nombre debe coincidir con el grupo de administración del proveedor externo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Ajustes",
|
||||
"subtitle": ""
|
||||
"subtitle": "Configurar ajustes del servidor."
|
||||
},
|
||||
"finish": {
|
||||
"title": "",
|
||||
"subtitle": "",
|
||||
"description": "",
|
||||
"action": {
|
||||
"goToBoard": "",
|
||||
"goToBoard": "Ir al tablero {name}",
|
||||
"createBoard": "",
|
||||
"inviteUser": "",
|
||||
"docs": ""
|
||||
@@ -109,7 +109,7 @@
|
||||
"name": "Usuario",
|
||||
"page": {
|
||||
"login": {
|
||||
"title": "",
|
||||
"title": "Inicia sesión en tu cuenta",
|
||||
"subtitle": ""
|
||||
},
|
||||
"invite": {
|
||||
@@ -160,12 +160,12 @@
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"usernameTaken": ""
|
||||
"usernameTaken": "Nombre de usuario ya está ocupado"
|
||||
},
|
||||
"action": {
|
||||
"login": {
|
||||
"label": "Iniciar sesión",
|
||||
"labelWith": "",
|
||||
"labelWith": "Iniciar sesión con {provider}",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
@@ -229,10 +229,10 @@
|
||||
"changeFirstDayOfWeek": {
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": ""
|
||||
"message": "Primer día de la semana cambiado con éxito"
|
||||
},
|
||||
"error": {
|
||||
"message": ""
|
||||
"message": "No se ha podido cambiar el primer día de la semana"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -251,7 +251,7 @@
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": ""
|
||||
"message": "La imagen ha sido cambiada con éxito"
|
||||
},
|
||||
"error": {
|
||||
"message": "No se puede cambiar la imagen"
|
||||
@@ -267,7 +267,7 @@
|
||||
"confirm": "Por favor, confirma que deseas eliminar esta imagen",
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": ""
|
||||
"message": "Imagen eliminada con éxito"
|
||||
},
|
||||
"error": {
|
||||
"message": ""
|
||||
@@ -278,7 +278,7 @@
|
||||
"editProfile": {
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": ""
|
||||
"message": "Perfil actualizado con éxito"
|
||||
},
|
||||
"error": {
|
||||
"message": ""
|
||||
@@ -288,10 +288,10 @@
|
||||
"delete": {
|
||||
"label": "",
|
||||
"description": "",
|
||||
"confirm": ""
|
||||
"confirm": "¿Estás seguro de que quieres eliminar el usuario {username} con sus preferencias?"
|
||||
},
|
||||
"select": {
|
||||
"label": "",
|
||||
"label": "Seleccionar usuario",
|
||||
"notFound": ""
|
||||
},
|
||||
"transfer": {
|
||||
@@ -1949,7 +1949,8 @@
|
||||
"pending": "",
|
||||
"approved": "",
|
||||
"declined": "",
|
||||
"failed": ""
|
||||
"failed": "",
|
||||
"completed": ""
|
||||
},
|
||||
"toBeDetermined": ""
|
||||
},
|
||||
|
||||
@@ -1949,7 +1949,8 @@
|
||||
"pending": "",
|
||||
"approved": "",
|
||||
"declined": "",
|
||||
"failed": ""
|
||||
"failed": "",
|
||||
"completed": ""
|
||||
},
|
||||
"toBeDetermined": ""
|
||||
},
|
||||
|
||||
@@ -1949,7 +1949,8 @@
|
||||
"pending": "En attente",
|
||||
"approved": "Approuvé",
|
||||
"declined": "Refusé",
|
||||
"failed": "Échec"
|
||||
"failed": "Échec",
|
||||
"completed": ""
|
||||
},
|
||||
"toBeDetermined": "À déterminer"
|
||||
},
|
||||
|
||||
@@ -1949,7 +1949,8 @@
|
||||
"pending": "ממתין",
|
||||
"approved": "אושר",
|
||||
"declined": "נדחה",
|
||||
"failed": "נכשל"
|
||||
"failed": "נכשל",
|
||||
"completed": ""
|
||||
},
|
||||
"toBeDetermined": "ייקבע בהמשך"
|
||||
},
|
||||
|
||||
@@ -1949,7 +1949,8 @@
|
||||
"pending": "",
|
||||
"approved": "",
|
||||
"declined": "",
|
||||
"failed": ""
|
||||
"failed": "",
|
||||
"completed": ""
|
||||
},
|
||||
"toBeDetermined": ""
|
||||
},
|
||||
|
||||
@@ -1949,7 +1949,8 @@
|
||||
"pending": "",
|
||||
"approved": "",
|
||||
"declined": "",
|
||||
"failed": ""
|
||||
"failed": "",
|
||||
"completed": ""
|
||||
},
|
||||
"toBeDetermined": ""
|
||||
},
|
||||
|
||||
@@ -1949,7 +1949,8 @@
|
||||
"pending": "",
|
||||
"approved": "",
|
||||
"declined": "",
|
||||
"failed": ""
|
||||
"failed": "",
|
||||
"completed": ""
|
||||
},
|
||||
"toBeDetermined": ""
|
||||
},
|
||||
|
||||
Loaded 100 of 137 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user