Compare commits

..
Author SHA1 Message Date
Meier Lukas 0b94219002 perf: try to improve nextjs dev performance 2025-05-27 22:46:53 +02:00
homarr-crowdin[bot] 1575716f41 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-05-27 17:02:51 +00:00
homarr-renovate[bot] 8966e2ad8a fix(deps): update dependency @t3-oss/env-nextjs to ^0.13.6 (#3244)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-27 12:36:02 +00:00
homarr-renovate[bot] 816544a996 fix(deps): update mantine monorepo to ^8.0.2 (#3245)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-27 11:19:17 +00:00
homarr-renovate[bot] 9c323850bc fix(deps): update dependency jotai to ^2.12.5 (#3243)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-27 09:21:29 +00:00
homarr-renovate[bot] c3936237c0 chore(deps): update dependency @types/react to v19.1.6 (#3242)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-27 08:26:07 +00:00
homarr-renovate[bot] 9b8d1cbb66 chore(deps): update dependency esbuild to ^0.25.5 (#3241)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-27 04:27:15 +00:00
homarr-renovate[bot] 3062948939 fix(deps): update dependency zod to ^3.25.30 (#3239)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-27 03:13:49 +00:00
homarr-renovate[bot] 78f98edc41 fix(deps): update trpc monorepo to ^11.1.3 (#3240)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-27 01:14:03 +00:00
homarr-renovate[bot] 81ca6ef7b2 fix(deps): update dependency zod to ^3.25.29 (#3237)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-26 23:20:16 +00:00
homarr-renovate[bot] 71186908b9 fix(deps): update tanstack-query monorepo to ^5.77.2 (#3232)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-26 12:34:40 +00:00
Meier Lukas cbbf0c8dcf fix: navigating back results in navigation back to first page opened (#3224) 2025-05-26 11:41:09 +02:00
Meier Lukas 1d27da2230 fix(calendar): remove scrollbar for 2x2 (#3227) 2025-05-26 11:40:42 +02:00
homarr-crowdin[bot] d4ef212042 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-05-26 00:53:16 +00:00
homarr-renovate[bot] 5ea7a20677 fix(deps): update tanstack-query monorepo to ^5.77.1 (#3229)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-25 22:19:07 +00:00
Meier Lukas 05e159f566 fix: csp prevents fonts to load (#3225) 2025-05-25 16:39:20 +02:00
homarr-renovate[bot] 6d9e42cf5d chore(deps): update dependency @semantic-release/github to ^11.0.3 (#3226)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-25 13:28:48 +00:00
Meier Lukas 437ecd1023 fix(calendar): load events for all visible days of calendar month (#3216) 2025-05-25 14:51:26 +02:00
Meier Lukas 85e98d2b60 fix(boards): app select modal images rendering issue (#3218) 2025-05-25 14:51:13 +02:00
homarr-crowdin[bot] 8e24d8362e chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-05-25 07:49:01 +00:00
Manuel c367f8ac23 docs: add pika pods (#3215) 2025-05-24 18:53:04 +02:00
Meier Lukas c67916f72e fix(downloads): upspeed font size wrong (#3214) 2025-05-24 18:52:55 +02:00
Meier Lukas 25bd3879db fix(media-request-stats): users squeezed together (#3213) 2025-05-24 18:52:45 +02:00
Meier Lukas 939df8f6d1 feat(cluster-health): add visibility options (#3210) 2025-05-24 17:49:49 +02:00
Meier Lukas 2dc871e531 feat(downloads): add option to limit amount of items (#3205) 2025-05-24 17:49:39 +02:00
Meier Lukas f7e5e823d5 fix(visual): check for creator board permission is wrong (#3212) 2025-05-24 17:49:08 +02:00
homarr-renovate[bot] 240bf16806 chore(deps): update dependency conventional-changelog-conventionalcommits to v9 (#3151)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-24 11:17:16 +02:00
homarr-renovate[bot] 640bf7b939 fix(deps): update dependency trpc-to-openapi to ^2.3.1 (#3046)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-24 09:14:40 +00:00
homarr-renovate[bot] ec42f265a0 fix(deps): update tanstack-query monorepo to ^5.77.0 (#3204)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-24 08:22:43 +00:00
homarr-crowdin[bot] ea5a763046 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-05-24 00:49:31 +00:00
homarr-renovate[bot] e0a9e4f97c fix(deps): update dependency zod to ^3.25.28 (#3201)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-23 23:20:33 +00:00
homarr-renovate[bot] 581746b2c2 chore(deps): update dependency semantic-release to ^24.2.5 (#3202)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-23 22:20:55 +00:00
ManuelandMeier Lukas be8b332f94 fix: reduce logging (#3200)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-05-23 22:36:23 +02:00
homarr-renovate[bot]andManuel 109f9e7979 fix(deps): update dependency node-cron to v4 (#3075)
Co-authored-by: Manuel <30572287+manuel-rw@users.noreply.github.com>
2025-05-23 22:20:47 +02:00
Meier Lukas 2afa9b6727 perf: cache middleware onboarding and language checks (#3197) 2025-05-23 22:20:21 +02:00
09af0a76f6 feat: allow custom protocols for app href (#2575)
Co-authored-by: Manuel <30572287+manuel-rw@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-05-23 20:19:59 +00:00
Manuel 71516e02c0 ci: ignore auto gen and build files eslint (#3198) 2025-05-23 19:47:44 +00:00
Meier Lukas a5f7565468 chore(issue-template): add version 1.21.0 2025-05-23 21:19:35 +02:00
homarr-renovate[bot] 0b224e8835 fix(deps): update dependency zod to ^3.25.24 (#3195)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-23 19:18:29 +00:00
102 changed files with 2260 additions and 1189 deletions

No files matched your search

+1
View File
@@ -31,6 +31,7 @@ body:
label: Version
description: What version of Homarr are you running?
options:
- 1.21.0
- 1.20.0
- 1.19.1
- 1.19.0
+41 -4
View File
@@ -17,7 +17,7 @@ const withNextIntl = createNextIntlPlugin({
requestConfig: "../../packages/translation/src/request.ts",
});
interface WebpackConfig {
interface _WebpackConfig {
module: {
rules: {
test: RegExp;
@@ -32,7 +32,7 @@ const nextConfig: NextConfig = {
/** We already do linting and typechecking as separate tasks in CI */
eslint: { ignoreDuringBuilds: true },
typescript: { ignoreBuildErrors: true },
webpack: (config: WebpackConfig, { isServer }) => {
/*webpack: (config: WebpackConfig, { isServer }) => {
if (isServer) {
config.module.rules.push({
test: /\.node$/,
@@ -41,14 +41,51 @@ const nextConfig: NextConfig = {
}
return config;
},
},*/
experimental: {
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
},
transpilePackages: ["@homarr/ui", "@homarr/notifications", "@homarr/modals", "@homarr/spotlight", "@homarr/widgets"],
serverExternalPackages: [
"dockerode",
"@kubernetes/client-node",
"@ctrl/deluge",
"@ctrl/qbittorrent",
"@ctrl/transmission",
"node-ical",
"tsdav",
"proxmox-api",
"@jellyfin/sdk",
],
images: {
domains: ["cdn.jsdelivr.net"],
},
// eslint-disable-next-line @typescript-eslint/require-await,no-restricted-syntax
async headers() {
return [
{
source: "/(.*)", // Apply CSP to all routes
headers: [
{
key: "Content-Security-Policy",
value: `
default-src 'self';
script-src * 'unsafe-inline' 'unsafe-eval';
base-uri 'self';
connect-src *;
style-src 'self' 'unsafe-inline';
frame-ancestors *;
frame-src *;
form-action 'self';
img-src * data:;
font-src * data:;
`
.replace(/\s{2,}/g, " ")
.trim(),
},
],
},
];
},
};
// Skip transform is used because of webpack loader, without it for example 'Tooltip.Floating' will not work and show an error
+16 -16
View File
@@ -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": "^8.0.1",
"@mantine/core": "^8.0.1",
"@mantine/dropzone": "^8.0.1",
"@mantine/hooks": "^8.0.1",
"@mantine/modals": "^8.0.1",
"@mantine/tiptap": "^8.0.1",
"@mantine/colors-generator": "^8.0.2",
"@mantine/core": "^8.0.2",
"@mantine/dropzone": "^8.0.2",
"@mantine/hooks": "^8.0.2",
"@mantine/modals": "^8.0.2",
"@mantine/tiptap": "^8.0.2",
"@million/lint": "1.0.14",
"@tabler/icons-react": "^3.33.0",
"@tanstack/react-query": "^5.76.2",
"@tanstack/react-query-devtools": "^5.76.2",
"@tanstack/react-query-next-experimental": "^5.76.2",
"@trpc/client": "^11.1.2",
"@trpc/next": "^11.1.2",
"@trpc/react-query": "^11.1.2",
"@trpc/server": "^11.1.2",
"@tanstack/react-query": "^5.77.2",
"@tanstack/react-query-devtools": "^5.77.2",
"@tanstack/react-query-next-experimental": "^5.77.2",
"@trpc/client": "^11.1.3",
"@trpc/next": "^11.1.3",
"@trpc/react-query": "^11.1.3",
"@trpc/server": "^11.1.3",
"@xterm/addon-canvas": "^0.7.0",
"@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "^5.5.0",
@@ -72,7 +72,7 @@
"dotenv": "^16.5.0",
"flag-icons": "^7.3.2",
"glob": "^11.0.2",
"jotai": "^2.12.4",
"jotai": "^2.12.5",
"mantine-react-table": "2.0.0-beta.9",
"next": "15.3.2",
"postcss-preset-mantine": "^1.17.0",
@@ -85,7 +85,7 @@
"superjson": "2.2.2",
"swagger-ui-react": "^5.22.0",
"use-deep-compare-effect": "^1.8.1",
"zod": "^3.25.23"
"zod": "^3.25.30"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
@@ -94,7 +94,7 @@
"@types/chroma-js": "3.1.1",
"@types/node": "^22.15.21",
"@types/prismjs": "^1.26.5",
"@types/react": "19.1.5",
"@types/react": "19.1.6",
"@types/react-dom": "19.1.5",
"@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.1.2",
@@ -1,6 +1,5 @@
"use client";
import type { MouseEvent } from "react";
import { useCallback, useEffect } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
@@ -204,17 +203,22 @@ const SelectBoardsMenu = () => {
);
};
const anchorSelector = "a[href]:not([target='_blank'])";
const usePreventLeaveWithDirty = (isDirty: boolean) => {
const t = useI18n();
const { openConfirmModal } = useConfirmModal();
const router = useRouter();
useEffect(() => {
const handleClick = (event: MouseEvent<HTMLElement>) => {
if (!isDirty) return;
const handleClick = (event: Event) => {
const target = (event.target as HTMLElement).closest("a");
if (!target) return;
if (!isDirty) return;
if (!target) {
console.warn("No anchor element found for click event", event);
return;
}
event.preventDefault();
@@ -231,33 +235,29 @@ const usePreventLeaveWithDirty = (isDirty: boolean) => {
};
const handlePopState = (event: Event) => {
if (isDirty) {
window.history.pushState(null, document.title, window.location.href);
event.preventDefault();
} else {
window.history.back();
}
window.history.pushState(null, document.title, window.location.href);
event.preventDefault();
};
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
if (!isDirty) return;
if (env.NODE_ENV === "development") return; // Allow to reload in development
event.preventDefault();
event.returnValue = true;
};
document.querySelectorAll("a").forEach((link) => {
link.addEventListener("click", handleClick as never);
const anchors = document.querySelectorAll(anchorSelector);
anchors.forEach((link) => {
link.addEventListener("click", handleClick);
});
window.addEventListener("popstate", handlePopState);
window.addEventListener("beforeunload", handleBeforeUnload);
return () => {
document.querySelectorAll("a").forEach((link) => {
link.removeEventListener("click", handleClick as never);
window.removeEventListener("popstate", handlePopState);
anchors.forEach((link) => {
link.removeEventListener("click", handleClick);
});
window.removeEventListener("popstate", handlePopState);
window.removeEventListener("beforeunload", handleBeforeUnload);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
+17 -6
View File
@@ -5,25 +5,36 @@ import SuperJSON from "superjson";
import type { AppRouter } from "@homarr/api";
import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared";
import { localeCookieKey } from "@homarr/definitions";
import type { SupportedLanguage } from "@homarr/translation";
import { supportedLanguages } from "@homarr/translation";
import { createI18nMiddleware } from "@homarr/translation/middleware";
export async function middleware(request: NextRequest) {
// fetch api does not work because window is not defined and we need to construct the url from the headers
// In next 15 we will be able to use node apis and such the db directly
const culture = await serverFetchApi.serverSettings.getCulture.query();
let isOnboardingFinished = false;
export async function middleware(request: NextRequest) {
// Redirect to onboarding if it's not finished yet
const pathname = request.nextUrl.pathname;
if (!pathname.endsWith("/init")) {
if (!isOnboardingFinished && !pathname.endsWith("/init")) {
const currentOnboardingStep = await serverFetchApi.onboard.currentStep.query();
if (currentOnboardingStep.current !== "finish") {
return NextResponse.redirect(new URL("/init", request.url));
}
isOnboardingFinished = true;
}
// Only run this if the user has not already configured their language
const currentLocale = request.cookies.get(localeCookieKey)?.value;
let defaultLocale: SupportedLanguage = "en";
if (!currentLocale || !supportedLanguages.includes(currentLocale as SupportedLanguage)) {
defaultLocale = await serverFetchApi.serverSettings.getCulture.query().then((culture) => culture.defaultLocale);
}
// We don't want to fallback to accept-language header so we clear it
request.headers.set("accept-language", "");
const next = createI18nMiddleware(culture.defaultLocale);
const next = createI18nMiddleware(defaultLocale);
return next(request);
}
+1 -1
View File
@@ -46,7 +46,7 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^22.15.21",
"dotenv-cli": "^8.0.0",
"esbuild": "^0.25.4",
"esbuild": "^0.25.5",
"eslint": "^9.27.0",
"prettier": "^3.5.3",
"tsx": "4.19.4",
+1 -1
View File
@@ -34,7 +34,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/ws": "^8.18.1",
"esbuild": "^0.25.4",
"esbuild": "^0.25.5",
"eslint": "^9.27.0",
"prettier": "^3.5.3",
"typescript": "^5.8.3"
+3 -1
View File
@@ -120,4 +120,6 @@ You can also support us by helping with [translating the entire project](https:/
Thanks to your generous sponsors we can continue to build Homarr. Check them out for high quality and easy to use development tools.
Feel free to contact us at homarr-labs@proton.me if you wish to become a sponsor.
[![Covered by Argos Visual Testing](https://argos-ci.com/badge-large.svg)](https://argos-ci.com?utm_source=%5Bhomarr%5D&utm_campaign=oss)
[![Covered by Argos Visual Testing](https://argos-ci.com/badge-large.svg)](https://argos-ci.com?utm_source=%5Bhomarr%5D&utm_campaign=oss) \
[![Supported by PikaPods](https://www.pikapods.com/static/run-button.svg)](https://www.pikapods.com/pods?run=homarr-v1)
+3 -3
View File
@@ -35,18 +35,18 @@
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^13.0.1",
"@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^11.0.2",
"@semantic-release/github": "^11.0.3",
"@semantic-release/npm": "^12.0.1",
"@semantic-release/release-notes-generator": "^14.0.3",
"@turbo/gen": "^2.5.3",
"@vitejs/plugin-react": "^4.5.0",
"@vitest/coverage-v8": "^3.1.4",
"@vitest/ui": "^3.1.4",
"conventional-changelog-conventionalcommits": "^8.0.0",
"conventional-changelog-conventionalcommits": "^9.0.0",
"cross-env": "^7.0.3",
"jsdom": "^26.1.0",
"prettier": "^3.5.3",
"semantic-release": "^24.2.4",
"semantic-release": "^24.2.5",
"testcontainers": "^10.28.0",
"turbo": "^2.5.3",
"typescript": "^5.8.3",
+7 -7
View File
@@ -41,18 +41,18 @@
"@homarr/server-settings": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@kubernetes/client-node": "^1.2.0",
"@tanstack/react-query": "^5.76.2",
"@trpc/client": "^11.1.2",
"@trpc/react-query": "^11.1.2",
"@trpc/server": "^11.1.2",
"@trpc/tanstack-react-query": "^11.1.2",
"@tanstack/react-query": "^5.77.2",
"@trpc/client": "^11.1.3",
"@trpc/react-query": "^11.1.3",
"@trpc/server": "^11.1.3",
"@trpc/tanstack-react-query": "^11.1.3",
"lodash.clonedeep": "^4.5.0",
"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.25.23"
"trpc-to-openapi": "^2.3.1",
"zod": "^3.25.30"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+25 -46
View File
@@ -1,52 +1,31 @@
import { apiKeysRouter } from "./router/apiKeys";
import { appRouter as innerAppRouter } from "./router/app";
import { boardRouter } from "./router/board";
import { certificateRouter } from "./router/certificates/certificate-router";
import { cronJobsRouter } from "./router/cron-jobs";
import { dockerRouter } from "./router/docker/docker-router";
import { groupRouter } from "./router/group";
import { homeRouter } from "./router/home";
import { iconsRouter } from "./router/icons";
import { importRouter } from "./router/import/import-router";
import { integrationRouter } from "./router/integration/integration-router";
import { inviteRouter } from "./router/invite";
import { kubernetesRouter } from "./router/kubernetes/router/kubernetes-router";
import { locationRouter } from "./router/location";
import { logRouter } from "./router/log";
import { mediaRouter } from "./router/medias/media-router";
import { onboardRouter } from "./router/onboard/onboard-router";
import { searchEngineRouter } from "./router/search-engine/search-engine-router";
import { sectionRouter } from "./router/section/section-router";
import { serverSettingsRouter } from "./router/serverSettings";
import { updateCheckerRouter } from "./router/update-checker";
import { userRouter } from "./router/user";
import { widgetRouter } from "./router/widgets";
import { lazy } from "@trpc/server";
import { createTRPCRouter } from "./trpc";
export const appRouter = createTRPCRouter({
user: userRouter,
group: groupRouter,
invite: inviteRouter,
integration: integrationRouter,
board: boardRouter,
section: sectionRouter,
app: innerAppRouter,
searchEngine: searchEngineRouter,
widget: widgetRouter,
location: locationRouter,
log: logRouter,
icon: iconsRouter,
import: importRouter,
onboard: onboardRouter,
home: homeRouter,
docker: dockerRouter,
kubernetes: kubernetesRouter,
serverSettings: serverSettingsRouter,
cronJobs: cronJobsRouter,
apiKeys: apiKeysRouter,
media: mediaRouter,
updateChecker: updateCheckerRouter,
certificates: certificateRouter,
user: lazy(() => import("./router/user").then((mod) => mod.userRouter)),
group: lazy(() => import("./router/group").then((mod) => mod.groupRouter)),
invite: lazy(() => import("./router/invite").then((mod) => mod.inviteRouter)),
integration: lazy(() => import("./router/integration/integration-router").then((mod) => mod.integrationRouter)),
board: lazy(() => import("./router/board").then((mod) => mod.boardRouter)),
section: lazy(() => import("./router/section/section-router").then((mod) => mod.sectionRouter)),
app: lazy(() => import("./router/app").then((mod) => mod.appRouter)),
searchEngine: lazy(() => import("./router/search-engine/search-engine-router").then((mod) => mod.searchEngineRouter)),
widget: lazy(() => import("./router/widgets").then((mod) => mod.widgetRouter)),
location: lazy(() => import("./router/location").then((mod) => mod.locationRouter)),
log: lazy(() => import("./router/log").then((mod) => mod.logRouter)),
icon: lazy(() => import("./router/icons").then((mod) => mod.iconsRouter)),
import: lazy(() => import("./router/import/import-router").then((mod) => mod.importRouter)),
onboard: lazy(() => import("./router/onboard/onboard-router").then((mod) => mod.onboardRouter)),
home: lazy(() => import("./router/home").then((mod) => mod.homeRouter)),
docker: lazy(() => import("./router/docker/docker-router").then((mod) => mod.dockerRouter)),
kubernetes: lazy(() => import("./router/kubernetes/router/kubernetes-router").then((mod) => mod.kubernetesRouter)),
serverSettings: lazy(() => import("./router/serverSettings").then((mod) => mod.serverSettingsRouter)),
cronJobs: lazy(() => import("./router/cron-jobs").then((mod) => mod.cronJobsRouter)),
apiKeys: lazy(() => import("./router/apiKeys").then((mod) => mod.apiKeysRouter)),
media: lazy(() => import("./router/medias/media-router").then((mod) => mod.mediaRouter)),
updateChecker: lazy(() => import("./router/update-checker").then((mod) => mod.updateCheckerRouter)),
certificates: lazy(() => import("./router/certificates/certificate-router").then((mod) => mod.certificateRouter)),
});
// export type definition of API
+8 -4
View File
@@ -19,10 +19,11 @@ const createDownloadClientIntegrationMiddleware = (action: IntegrationAction) =>
export const downloadsRouter = createTRPCRouter({
getJobsAndStatuses: publicProcedure
.concat(createDownloadClientIntegrationMiddleware("query"))
.query(async ({ ctx }) => {
.input(z.object({ limitPerIntegration: z.number().default(50) }))
.query(async ({ ctx, input }) => {
return await Promise.all(
ctx.integrations.map(async (integration) => {
const innerHandler = downloadClientRequestHandler.handler(integration, {});
const innerHandler = downloadClientRequestHandler.handler(integration, { limit: input.limitPerIntegration });
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
@@ -40,7 +41,8 @@ export const downloadsRouter = createTRPCRouter({
}),
subscribeToJobsAndStatuses: publicProcedure
.concat(createDownloadClientIntegrationMiddleware("query"))
.subscription(({ ctx }) => {
.input(z.object({ limitPerIntegration: z.number().default(50) }))
.subscription(({ ctx, input }) => {
return observable<{
integration: Modify<Integration, { kind: IntegrationKindByCategory<"downloadClient"> }>;
data: DownloadClientJobsAndStatus;
@@ -48,7 +50,9 @@ export const downloadsRouter = createTRPCRouter({
const unsubscribes: (() => void)[] = [];
for (const integrationWithSecrets of ctx.integrations) {
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
const innerHandler = downloadClientRequestHandler.handler(integrationWithSecrets, {});
const innerHandler = downloadClientRequestHandler.handler(integrationWithSecrets, {
limit: input.limitPerIntegration,
});
const unsubscribe = innerHandler.subscribe((data) => {
emit.next({
integration,
+20 -36
View File
@@ -1,40 +1,24 @@
import { lazy } from "@trpc/server";
import { createTRPCRouter } from "../../trpc";
import { appRouter } from "./app";
import { calendarRouter } from "./calendar";
import { dnsHoleRouter } from "./dns-hole";
import { downloadsRouter } from "./downloads";
import { healthMonitoringRouter } from "./health-monitoring";
import { indexerManagerRouter } from "./indexer-manager";
import { mediaRequestsRouter } from "./media-requests";
import { mediaServerRouter } from "./media-server";
import { mediaTranscodingRouter } from "./media-transcoding";
import { minecraftRouter } from "./minecraft";
import { networkControllerRouter } from "./network-controller";
import { notebookRouter } from "./notebook";
import { optionsRouter } from "./options";
import { releasesRouter } from "./releases";
import { rssFeedRouter } from "./rssFeed";
import { smartHomeRouter } from "./smart-home";
import { stockPriceRouter } from "./stocks";
import { weatherRouter } from "./weather";
export const widgetRouter = createTRPCRouter({
notebook: notebookRouter,
weather: weatherRouter,
app: appRouter,
dnsHole: dnsHoleRouter,
smartHome: smartHomeRouter,
stockPrice: stockPriceRouter,
mediaServer: mediaServerRouter,
calendar: calendarRouter,
downloads: downloadsRouter,
mediaRequests: mediaRequestsRouter,
rssFeed: rssFeedRouter,
indexerManager: indexerManagerRouter,
healthMonitoring: healthMonitoringRouter,
mediaTranscoding: mediaTranscodingRouter,
minecraft: minecraftRouter,
options: optionsRouter,
releases: releasesRouter,
networkController: networkControllerRouter,
notebook: lazy(() => import("./notebook").then((mod) => mod.notebookRouter)),
weather: lazy(() => import("./weather").then((mod) => mod.weatherRouter)),
app: lazy(() => import("./app").then((mod) => mod.appRouter)),
dnsHole: lazy(() => import("./dns-hole").then((mod) => mod.dnsHoleRouter)),
smartHome: lazy(() => import("./smart-home").then((mod) => mod.smartHomeRouter)),
stockPrice: lazy(() => import("./stocks").then((mod) => mod.stockPriceRouter)),
mediaServer: lazy(() => import("./media-server").then((mod) => mod.mediaServerRouter)),
calendar: lazy(() => import("./calendar").then((mod) => mod.calendarRouter)),
downloads: lazy(() => import("./downloads").then((mod) => mod.downloadsRouter)),
mediaRequests: lazy(() => import("./media-requests").then((mod) => mod.mediaRequestsRouter)),
rssFeed: lazy(() => import("./rssFeed").then((mod) => mod.rssFeedRouter)),
indexerManager: lazy(() => import("./indexer-manager").then((mod) => mod.indexerManagerRouter)),
healthMonitoring: lazy(() => import("./health-monitoring").then((mod) => mod.healthMonitoringRouter)),
mediaTranscoding: lazy(() => import("./media-transcoding").then((mod) => mod.mediaTranscodingRouter)),
minecraft: lazy(() => import("./minecraft").then((mod) => mod.minecraftRouter)),
options: lazy(() => import("./options").then((mod) => mod.optionsRouter)),
releases: lazy(() => import("./releases").then((mod) => mod.releasesRouter)),
networkController: lazy(() => import("./network-controller").then((mod) => mod.networkControllerRouter)),
});
+1 -1
View File
@@ -39,7 +39,7 @@
"next-auth": "5.0.0-beta.28",
"react": "19.1.0",
"react-dom": "19.1.0",
"zod": "^3.25.23"
"zod": "^3.25.30"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
@@ -23,21 +23,22 @@ export type BoardPermissionsProps = (
export const constructBoardPermissions = (board: BoardPermissionsProps, session: Session | null) => {
const creatorId = "creator" in board ? board.creator?.id : board.creatorId;
const isCreator = session !== null && session.user.id === creatorId;
return {
hasFullAccess:
session?.user.id === creatorId ||
isCreator ||
board.userPermissions.some(({ permission }) => permission === "full") ||
board.groupPermissions.some(({ permission }) => permission === "full") ||
(session?.user.permissions.includes("board-full-all") ?? false),
hasChangeAccess:
session?.user.id === creatorId ||
isCreator ||
board.userPermissions.some(({ permission }) => permission === "modify" || permission === "full") ||
board.groupPermissions.some(({ permission }) => permission === "modify" || permission === "full") ||
(session?.user.permissions.includes("board-modify-all") ?? false) ||
(session?.user.permissions.includes("board-full-all") ?? false),
hasViewAccess:
session?.user.id === creatorId ||
isCreator ||
board.userPermissions.length >= 1 ||
board.groupPermissions.length >= 1 ||
board.isPublic ||
@@ -286,4 +286,22 @@ describe("constructBoardPermissions", () => {
expect(result.hasChangeAccess).toBe(false);
expect(result.hasViewAccess).toBe(true);
});
test("should return all false when creator is null and session is null", () => {
// Arrange
const board = {
creator: null,
userPermissions: [],
groupPermissions: [],
isPublic: false,
};
const session = null;
// Act
const result = constructBoardPermissions(board, session);
// Assert
expect(result.hasFullAccess).toBe(false);
expect(result.hasChangeAccess).toBe(false);
expect(result.hasViewAccess).toBe(false);
});
});
+1 -1
View File
@@ -34,7 +34,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"esbuild": "^0.25.4",
"esbuild": "^0.25.5",
"eslint": "^9.27.0",
"typescript": "^5.8.3"
}
+1 -1
View File
@@ -34,7 +34,7 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"undici": "7.10.0",
"zod": "^3.25.23",
"zod": "^3.25.30",
"zod-validation-error": "^3.4.1"
},
"devDependencies": {
+1 -1
View File
@@ -24,7 +24,7 @@ export class LoggingAgent extends Agent {
url.searchParams.set(key, "REDACTED");
});
logger.info(
logger.debug(
`Dispatching request ${url.toString().replaceAll("=&", "&")} (${Object.keys(options.headers ?? {}).length} headers)`,
);
return super.dispatch(options, handler);
+4 -4
View File
@@ -21,7 +21,7 @@ const REDACTED = "REDACTED";
describe("LoggingAgent should log all requests", () => {
test("should log all requests", () => {
// Arrange
const infoLogSpy = vi.spyOn(logger, "info");
const infoLogSpy = vi.spyOn(logger, "debug");
const agent = new LoggingAgent();
// Act
@@ -33,7 +33,7 @@ describe("LoggingAgent should log all requests", () => {
test("should show amount of headers", () => {
// Arrange
const infoLogSpy = vi.spyOn(logger, "info");
const infoLogSpy = vi.spyOn(logger, "debug");
const agent = new LoggingAgent();
// Act
@@ -68,7 +68,7 @@ describe("LoggingAgent should log all requests", () => {
[`/?stringWith13Chars=${"a".repeat(13)}`, `/?stringWith13Chars=${REDACTED}`],
])("should redact sensitive data in url https://homarr.dev%s", (path, expected) => {
// Arrange
const infoLogSpy = vi.spyOn(logger, "info");
const infoLogSpy = vi.spyOn(logger, "debug");
const agent = new LoggingAgent();
// Act
@@ -87,7 +87,7 @@ describe("LoggingAgent should log all requests", () => {
["date times", "/?datetime=2022-01-01T00:00:00.000Z"],
])("should not redact values that are %s", (_reason, path) => {
// Arrange
const infoLogSpy = vi.spyOn(logger, "info");
const infoLogSpy = vi.spyOn(logger, "debug");
const agent = new LoggingAgent();
// Act
+1 -1
View File
@@ -7,6 +7,6 @@ import { cronJobRunnerChannel } from ".";
*/
export const registerCronJobRunner = () => {
cronJobRunnerChannel.subscribe((jobName) => {
jobGroup.runManually(jobName);
void jobGroup.runManuallyAsync(jobName);
});
};
+1 -1
View File
@@ -25,7 +25,7 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"node-cron": "^3.0.3"
"node-cron": "^4.0.7"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+7 -7
View File
@@ -1,5 +1,6 @@
import { AxiosError } from "axios";
import cron from "node-cron";
import type { ScheduledTask } from "node-cron";
import { schedule, validate } from "node-cron";
import { Stopwatch } from "@homarr/common";
import type { MaybePromise } from "@homarr/common/types";
@@ -27,7 +28,7 @@ const createCallback = <TAllowedNames extends string, TName extends TAllowedName
options: CreateCronJobOptions,
creatorOptions: CreateCronJobCreatorOptions<TAllowedNames>,
) => {
const expectedMaximumDurationInMillis = options.expectedMaximumDurationInMillis ?? 1000;
const expectedMaximumDurationInMillis = options.expectedMaximumDurationInMillis ?? 2500;
return (callback: () => MaybePromise<void>) => {
const catchingCallbackAsync = async () => {
try {
@@ -64,12 +65,11 @@ const createCallback = <TAllowedNames extends string, TName extends TAllowedName
/**
* We are not using the runOnInit method as we want to run the job only once we start the cron job schedule manually.
* This allows us to always run it once we start it. Additionally it will not run the callback if only the cron job file is imported.
* This allows us to always run it once we start it. Additionally, it will not run the callback if only the cron job file is imported.
*/
let scheduledTask: cron.ScheduledTask | null = null;
let scheduledTask: ScheduledTask | null = null;
if (cronExpression !== "never") {
scheduledTask = cron.schedule(cronExpression, () => void catchingCallbackAsync(), {
scheduled: false,
scheduledTask = schedule(cronExpression, () => void catchingCallbackAsync(), {
name,
timezone: creatorOptions.timezone,
});
@@ -110,7 +110,7 @@ export const createCronJobCreator = <TAllowedNames extends string = string>(
options: CreateCronJobOptions = { runOnStart: false },
) => {
creatorOptions.logger.logDebug(`Validating cron expression '${cronExpression}' for job: ${name}`);
if (cronExpression !== "never" && !cron.validate(cronExpression)) {
if (cronExpression !== "never" && !validate(cronExpression)) {
throw new Error(`Invalid cron expression '${cronExpression}' for job '${name}'`);
}
creatorOptions.logger.logDebug(`Cron job expression '${cronExpression}' for job ${name} is valid`);
+8 -8
View File
@@ -34,33 +34,33 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
options.logger.logInfo(`Starting schedule cron job ${job.name}.`);
await job.onStartAsync();
job.scheduledTask?.start();
await job.scheduledTask?.start();
},
startAllAsync: async () => {
for (const job of jobRegistry.values()) {
options.logger.logInfo(`Starting schedule of cron job ${job.name}.`);
await job.onStartAsync();
job.scheduledTask?.start();
await job.scheduledTask?.start();
}
},
runManually: (name: keyof TJobs) => {
runManuallyAsync: async (name: keyof TJobs) => {
const job = jobRegistry.get(name as string);
if (!job) return;
options.logger.logInfo(`Running schedule cron job ${job.name} manually.`);
job.scheduledTask?.now();
await job.scheduledTask?.execute();
},
stop: (name: keyof TJobs) => {
stopAsync: async (name: keyof TJobs) => {
const job = jobRegistry.get(name as string);
if (!job) return;
options.logger.logInfo(`Stopping schedule cron job ${job.name}.`);
job.scheduledTask?.stop();
await job.scheduledTask?.stop();
},
stopAll: () => {
stopAllAsync: async () => {
for (const job of jobRegistry.values()) {
options.logger.logInfo(`Stopping schedule cron job ${job.name}.`);
job.scheduledTask?.stop();
await job.scheduledTask?.stop();
}
},
getJobRegistry() {
@@ -8,7 +8,9 @@ export const downloadsJob = createCronJob("downloads", EVERY_5_SECONDS).withCall
createRequestIntegrationJobHandler(downloadClientRequestHandler.handler, {
widgetKinds: ["downloads"],
getInput: {
downloads: () => ({}),
downloads: (options) => ({
limit: options.limitPerIntegration,
}),
},
}),
);
+2 -2
View File
@@ -44,7 +44,7 @@
"@homarr/env": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@mantine/core": "^8.0.1",
"@mantine/core": "^8.0.2",
"@paralleldrive/cuid2": "^2.2.2",
"@testcontainers/mysql": "^10.28.0",
"better-sqlite3": "^11.10.0",
@@ -60,7 +60,7 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/better-sqlite3": "7.6.13",
"dotenv-cli": "^8.0.0",
"esbuild": "^0.25.4",
"esbuild": "^0.25.5",
"eslint": "^9.27.0",
"prettier": "^3.5.3",
"tsx": "4.19.4",
+1 -1
View File
@@ -25,7 +25,7 @@
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"fast-xml-parser": "^5.2.3",
"zod": "^3.25.23"
"zod": "^3.25.30"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+2 -2
View File
@@ -23,8 +23,8 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@t3-oss/env-nextjs": "^0.13.4",
"zod": "^3.25.23"
"@t3-oss/env-nextjs": "^0.13.6",
"zod": "^3.25.30"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+2 -2
View File
@@ -26,8 +26,8 @@
"@homarr/common": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/form": "^8.0.1",
"zod": "^3.25.23"
"@mantine/form": "^8.0.2",
"zod": "^3.25.30"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+2 -2
View File
@@ -29,9 +29,9 @@
"@homarr/notifications": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.0.1",
"@mantine/core": "^8.0.2",
"react": "19.1.0",
"zod": "^3.25.23"
"zod": "^3.25.30"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+1 -1
View File
@@ -44,7 +44,7 @@
"tsdav": "^2.1.4",
"undici": "7.10.0",
"xml2js": "^0.6.2",
"zod": "^3.25.23"
"zod": "^3.25.30"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
@@ -12,7 +12,7 @@ import type { DownloadClientItem } from "../../interfaces/downloads/download-cli
import type { Aria2Download, Aria2GetClient } from "./aria2-types";
export class Aria2Integration extends DownloadClientIntegration {
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
const client = this.getClient();
const keys: (keyof Aria2Download)[] = [
"bittorrent",
@@ -27,12 +27,12 @@ export class Aria2Integration extends DownloadClientIntegration {
];
const [activeDownloads, waitingDownloads, stoppedDownloads, globalStats] = await Promise.all([
client.tellActive(),
client.tellWaiting(0, 1000, keys),
client.tellStopped(0, 1000, keys),
client.tellWaiting(0, input.limit, keys),
client.tellStopped(0, input.limit, keys),
client.getGlobalStat(),
]);
const downloads = [...activeDownloads, ...waitingDownloads, ...stoppedDownloads];
const downloads = [...activeDownloads, ...waitingDownloads, ...stoppedDownloads].slice(0, input.limit);
const allPaused = downloads.every((download) => download.status === "paused");
return {
@@ -29,9 +29,10 @@ export class DelugeIntegration extends DownloadClientIntegration {
};
}
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
const type = "torrent";
const client = await this.getClientAsync();
// Currently there is no way to limit the number of returned torrents
const {
stats: { download_rate, upload_rate },
torrents: rawTorrents,
@@ -49,27 +50,29 @@ export class DelugeIntegration extends DownloadClientIntegration {
},
types: [type],
};
const items = torrents.map((torrent): DownloadClientItem => {
const state = DelugeIntegration.getTorrentState(torrent.state);
return {
type,
id: torrent.id,
index: torrent.queue,
name: torrent.name,
size: torrent.total_wanted,
sent: torrent.total_uploaded,
downSpeed: torrent.progress !== 100 ? torrent.download_payload_rate : undefined,
upSpeed: torrent.upload_payload_rate,
time:
torrent.progress === 100
? Math.min((torrent.completed_time - dayjs().unix()) * 1000, -1)
: Math.max(torrent.eta * 1000, 0),
added: torrent.time_added * 1000,
state,
progress: torrent.progress / 100,
category: torrent.label,
};
});
const items = torrents
.map((torrent): DownloadClientItem => {
const state = DelugeIntegration.getTorrentState(torrent.state);
return {
type,
id: torrent.id,
index: torrent.queue,
name: torrent.name,
size: torrent.total_wanted,
sent: torrent.total_uploaded,
downSpeed: torrent.progress !== 100 ? torrent.download_payload_rate : undefined,
upSpeed: torrent.upload_payload_rate,
time:
torrent.progress === 100
? Math.min((torrent.completed_time - dayjs().unix()) * 1000, -1)
: Math.max(torrent.eta * 1000, 0),
added: torrent.time_added * 1000,
state,
progress: torrent.progress / 100,
category: torrent.label,
};
})
.slice(0, input.limit);
return { status, items };
}
@@ -20,7 +20,7 @@ export class NzbGetIntegration extends DownloadClientIntegration {
};
}
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
const type = "usenet";
const queue = await this.nzbGetApiCallAsync("listgroups");
const history = await this.nzbGetApiCallAsync("history");
@@ -65,7 +65,8 @@ export class NzbGetIntegration extends DownloadClientIntegration {
category: file.Category,
};
}),
);
)
.slice(0, input.limit);
return { status, items };
}
@@ -26,10 +26,10 @@ export class QBitTorrentIntegration extends DownloadClientIntegration {
};
}
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
const type = "torrent";
const client = await this.getClientAsync();
const torrents = await client.listTorrents();
const torrents = await client.listTorrents({ limit: input.limit });
const rates = torrents.reduce(
({ down, up }, { dlspeed, upspeed }) => ({ down: down + dlspeed, up: up + upspeed }),
{ down: 0, up: 0 },
@@ -22,10 +22,14 @@ export class SabnzbdIntegration extends DownloadClientIntegration {
return { success: true };
}
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
const type = "usenet";
const { queue } = await queueSchema.parseAsync(await this.sabNzbApiCallAsync("queue"));
const { history } = await historySchema.parseAsync(await this.sabNzbApiCallAsync("history"));
const { queue } = await queueSchema.parseAsync(
await this.sabNzbApiCallAsync("queue", { limit: input.limit.toString() }),
);
const { history } = await historySchema.parseAsync(
await this.sabNzbApiCallAsync("history", { limit: input.limit.toString() }),
);
const status: DownloadClientStatus = {
paused: queue.paused,
rates: { down: Math.floor(Number(queue.kbpersec) * 1024) }, //Actually rounded kiBps ()
@@ -73,7 +77,8 @@ export class SabnzbdIntegration extends DownloadClientIntegration {
category: slot.category,
};
}),
);
)
.slice(0, input.limit);
return { status, items };
}
@@ -23,9 +23,10 @@ export class TransmissionIntegration extends DownloadClientIntegration {
};
}
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
const type = "torrent";
const client = await this.getClientAsync();
// Currently there is no way to limit the number of returned torrents
const { torrents } = (await client.listTorrents()).arguments;
const rates = torrents.reduce(
({ down, up }, { rateDownload, rateUpload }) => ({ down: down + rateDownload, up: up + rateUpload }),
@@ -34,27 +35,29 @@ export class TransmissionIntegration extends DownloadClientIntegration {
const paused =
torrents.find(({ status }) => TransmissionIntegration.getTorrentState(status) !== "paused") === undefined;
const status: DownloadClientStatus = { paused, rates, types: [type] };
const items = torrents.map((torrent): DownloadClientItem => {
const state = TransmissionIntegration.getTorrentState(torrent.status);
return {
type,
id: torrent.hashString,
index: torrent.queuePosition,
name: torrent.name,
size: torrent.totalSize,
sent: torrent.uploadedEver,
downSpeed: torrent.percentDone !== 1 ? torrent.rateDownload : undefined,
upSpeed: torrent.rateUpload,
time:
torrent.percentDone === 1
? Math.min(torrent.doneDate * 1000 - dayjs().valueOf(), -1)
: Math.max(torrent.eta * 1000, 0),
added: torrent.addedDate * 1000,
state,
progress: torrent.percentDone,
category: torrent.labels,
};
});
const items = torrents
.map((torrent): DownloadClientItem => {
const state = TransmissionIntegration.getTorrentState(torrent.status);
return {
type,
id: torrent.hashString,
index: torrent.queuePosition,
name: torrent.name,
size: torrent.totalSize,
sent: torrent.uploadedEver,
downSpeed: torrent.percentDone !== 1 ? torrent.rateDownload : undefined,
upSpeed: torrent.rateUpload,
time:
torrent.percentDone === 1
? Math.min(torrent.doneDate * 1000 - dayjs().valueOf(), -1)
: Math.max(torrent.eta * 1000, 0),
added: torrent.addedDate * 1000,
state,
progress: torrent.percentDone,
category: torrent.labels,
};
})
.slice(0, input.limit);
return { status, items };
}
@@ -4,7 +4,7 @@ import type { DownloadClientItem } from "./download-client-items";
export abstract class DownloadClientIntegration extends Integration {
/** Get download client's status and list of all of it's items */
public abstract getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus>;
public abstract getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus>;
/** Pauses the client or all of it's items */
public abstract pauseQueueAsync(): Promise<void>;
/** Pause a single item using it's ID */
@@ -184,21 +184,20 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
public async approveRequestAsync(requestId: number): Promise<void> {
logger.info(`Approving media request id='${requestId}' integration='${this.integration.name}'`);
const response = await fetchWithTrustedCertificatesAsync(this.url(`/api/v1/request/${requestId}/approve`), {
await fetchWithTrustedCertificatesAsync(this.url(`/api/v1/request/${requestId}/approve`), {
method: "POST",
headers: {
"X-Api-Key": this.getSecretValue("apiKey"),
},
}).then((response) => {
if (!response.ok) {
logger.error(
`Failed to approve media request id='${requestId}' integration='${this.integration.name}' reason='${response.status} ${response.statusText}' url='${response.url}'`,
);
}
logger.info(`Successfully approved media request id='${requestId}' integration='${this.integration.name}'`);
});
if (!response.ok) {
const content = await response.text();
logger.error(
`Failed to approve media request id='${requestId}' integration='${this.integration.name}' reason='${response.status} ${response.statusText}' url='${response.url}' content='${content}'`,
);
}
logger.info(`Successfully approved media request id='${requestId}' integration='${this.integration.name}'`);
}
public async declineRequestAsync(requestId: number): Promise<void> {
+5 -5
View File
@@ -46,7 +46,7 @@ describe("Aria2 integration", () => {
// Acts
const actAsync = async () => await aria2Integration.pauseQueueAsync();
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync();
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(actAsync()).resolves.not.toThrow();
@@ -62,7 +62,7 @@ describe("Aria2 integration", () => {
const aria2Integration = createAria2Intergration(startedContainer, API_KEY);
// Act
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync();
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(getAsync()).resolves.not.toThrow();
@@ -81,7 +81,7 @@ describe("Aria2 integration", () => {
await aria2AddItemAsync(startedContainer, API_KEY, aria2Integration);
// Act
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync();
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(getAsync()).resolves.not.toThrow();
@@ -104,7 +104,7 @@ describe("Aria2 integration", () => {
await expect(actAsync()).resolves.not.toThrow();
// NzbGet is slow and we wait for a second before querying the items. Test was flaky without this.
await new Promise((resolve) => setTimeout(resolve, 1000));
const result = await aria2Integration.getClientJobsAndStatusAsync();
const result = await aria2Integration.getClientJobsAndStatusAsync({ limit: 99 });
expect(result.items).toHaveLength(0);
// Cleanup
@@ -153,7 +153,7 @@ const aria2AddItemAsync = async (container: StartedTestContainer, apiKey: string
const {
items: [item],
} = await integration.getClientJobsAndStatusAsync();
} = await integration.getClientJobsAndStatusAsync({ limit: 99 });
if (!item) {
throw new Error("No item found");
+6 -6
View File
@@ -69,7 +69,7 @@ describe("Nzbget integration", () => {
// Acts
const actAsync = async () => await nzbGetIntegration.pauseQueueAsync();
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync();
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(actAsync()).resolves.not.toThrow();
@@ -87,7 +87,7 @@ describe("Nzbget integration", () => {
// Acts
const actAsync = async () => await nzbGetIntegration.resumeQueueAsync();
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync();
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(actAsync()).resolves.not.toThrow();
@@ -105,7 +105,7 @@ describe("Nzbget integration", () => {
const nzbGetIntegration = createNzbGetIntegration(startedContainer, username, password);
// Act
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync();
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(getAsync()).resolves.not.toThrow();
@@ -124,7 +124,7 @@ describe("Nzbget integration", () => {
await nzbGetAddItemAsync(startedContainer, username, password, nzbGetIntegration);
// Act
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync();
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(getAsync()).resolves.not.toThrow();
@@ -147,7 +147,7 @@ describe("Nzbget integration", () => {
await expect(actAsync()).resolves.not.toThrow();
// NzbGet is slow and we wait for a second before querying the items. Test was flaky without this.
await new Promise((resolve) => setTimeout(resolve, 1000));
const result = await nzbGetIntegration.getClientJobsAndStatusAsync();
const result = await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 });
expect(result.items).toHaveLength(0);
// Cleanup
@@ -209,7 +209,7 @@ const nzbGetAddItemAsync = async (
const {
items: [item],
} = await integration.getClientJobsAndStatusAsync();
} = await integration.getClientJobsAndStatusAsync({ limit: 99 });
if (!item) {
throw new Error("No item found");
+8 -8
View File
@@ -67,7 +67,7 @@ describe("Sabnzbd integration", () => {
// Acts
const actAsync = async () => await sabnzbdIntegration.pauseQueueAsync();
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(actAsync()).resolves.not.toThrow();
@@ -85,7 +85,7 @@ describe("Sabnzbd integration", () => {
// Acts
const actAsync = async () => await sabnzbdIntegration.resumeQueueAsync();
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(actAsync()).resolves.not.toThrow();
@@ -103,7 +103,7 @@ describe("Sabnzbd integration", () => {
const sabnzbdIntegration = createSabnzbdIntegration(startedContainer, DEFAULT_API_KEY);
// Act
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(getAsync()).resolves.not.toThrow();
@@ -122,7 +122,7 @@ describe("Sabnzbd integration", () => {
await sabNzbdAddItemAsync(startedContainer, DEFAULT_API_KEY, sabnzbdIntegration);
// Act
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(getAsync()).resolves.not.toThrow();
@@ -140,7 +140,7 @@ describe("Sabnzbd integration", () => {
// Act
const actAsync = async () => await sabnzbdIntegration.pauseItemAsync(item);
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(getAsync()).resolves.toMatchObject({ items: [{ ...item, state: "downloading" }] });
@@ -160,7 +160,7 @@ describe("Sabnzbd integration", () => {
// Act
const actAsync = async () => await sabnzbdIntegration.resumeItemAsync(item);
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(getAsync()).resolves.toMatchObject({ items: [{ ...item, state: "paused" }] });
@@ -180,7 +180,7 @@ describe("Sabnzbd integration", () => {
// Act - fromDisk already doesn't work for sabnzbd, so only test deletion itself.
const actAsync = async () =>
await sabnzbdIntegration.deleteItemAsync({ ...item, progress: 0 } as DownloadClientItem, false);
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(actAsync()).resolves.not.toThrow();
@@ -242,7 +242,7 @@ const sabNzbdAddItemAsync = async (
for (let i = 0; i < 5; i++) {
const {
items: [item],
} = await integration.getClientJobsAndStatusAsync();
} = await integration.getClientJobsAndStatusAsync({ limit: 99 });
if (item) return item;
}
// Throws if it can't find the item
+1 -1
View File
@@ -27,7 +27,7 @@
"ioredis": "5.6.1",
"superjson": "2.2.2",
"winston": "3.17.0",
"zod": "^3.25.23"
"zod": "^3.25.30"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+2 -2
View File
@@ -33,13 +33,13 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.0.1",
"@mantine/core": "^8.0.2",
"@tabler/icons-react": "^3.33.0",
"dayjs": "^1.11.13",
"next": "15.3.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"zod": "^3.25.23"
"zod": "^3.25.30"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
@@ -1,5 +1,4 @@
import { useMemo, useState } from "react";
import Image from "next/image";
import { Button, Card, Center, Grid, Input, Stack, Text } from "@mantine/core";
import { IconPlus, IconSearch } from "@tabler/icons-react";
@@ -23,7 +22,7 @@ export const AppSelectModal = createModal<AppSelectModalProps>(({ actions, inner
() =>
apps
.filter((app) => app.name.toLowerCase().includes(search.toLowerCase()))
.sort((a, b) => a.name.localeCompare(b.name)),
.sort((appA, appB) => appA.name.localeCompare(appB.name)),
[apps, search],
);
@@ -88,7 +87,7 @@ export const AppSelectModal = createModal<AppSelectModalProps>(({ actions, inner
<Stack justify="space-between" h="100%">
<Stack gap="xs">
<Center>
<Image src={app.iconUrl || ""} alt={app.name} width={24} height={24} />
<img src={app.iconUrl} alt={app.name} width={24} height={24} />
</Center>
<Text lh={1.2} style={{ whiteSpace: "normal" }} ta="center">
{app.name}
+2 -2
View File
@@ -24,8 +24,8 @@
"dependencies": {
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^8.0.1",
"@mantine/hooks": "^8.0.1",
"@mantine/core": "^8.0.2",
"@mantine/hooks": "^8.0.2",
"react": "19.1.0"
},
"devDependencies": {
+1 -1
View File
@@ -24,7 +24,7 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/ui": "workspace:^0.1.0",
"@mantine/notifications": "^8.0.1",
"@mantine/notifications": "^8.0.2",
"@tabler/icons-react": "^3.33.0"
},
"devDependencies": {
+3 -3
View File
@@ -37,14 +37,14 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.0.1",
"@mantine/hooks": "^8.0.1",
"@mantine/core": "^8.0.2",
"@mantine/hooks": "^8.0.2",
"adm-zip": "0.5.16",
"next": "15.3.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"superjson": "2.2.2",
"zod": "^3.25.23",
"zod": "^3.25.30",
"zod-form-data": "^2.0.7"
},
"devDependencies": {
@@ -77,6 +77,7 @@ const optionMapping: OptionMapping = {
descendingDefaultSort: () => false,
showCompletedUsenet: () => true,
showCompletedHttp: () => true,
limitPerIntegration: () => undefined,
},
weather: {
forecastDayCount: (oldOptions) => oldOptions.forecastDays,
@@ -156,6 +157,21 @@ const optionMapping: OptionMapping = {
defaultTab: (oldOptions) => ("defaultTabState" in oldOptions ? oldOptions.defaultTabState : undefined),
sectionIndicatorRequirement: (oldOptions) =>
"sectionIndicatorColor" in oldOptions ? oldOptions.sectionIndicatorColor : undefined,
showUptime: () => undefined,
visibleClusterSections: (oldOptions) => {
if (!("showNode" in oldOptions)) return undefined;
const oldKeys = {
showNode: "node" as const,
showLXCs: "lxc" as const,
showVM: "qemu" as const,
showStorage: "storage" as const,
} satisfies Partial<Record<keyof typeof oldOptions, string>>;
return objectEntries(oldKeys)
.filter(([key]) => oldOptions[key])
.map(([_, section]) => section);
},
},
mediaTranscoding: {
defaultView: (oldOptions) => oldOptions.defaultView,
+1 -1
View File
@@ -23,7 +23,7 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"zod": "^3.25.23"
"zod": "^3.25.30"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+4 -2
View File
@@ -13,8 +13,10 @@ export const calendarMonthRequestHandler = createCachedIntegrationRequestHandler
>({
async requestAsync(integration, input) {
const integrationInstance = await createIntegrationAsync(integration);
const startDate = dayjs().year(input.year).month(input.month).startOf("month");
const endDate = startDate.clone().endOf("month");
// Calendar component shows up to 6 days before and after the month, for example if 1. of january is sunday, it shows the last 6 days of december.
const startDate = dayjs().year(input.year).month(input.month).startOf("month").subtract(6, "days");
const endDate = dayjs().year(input.year).month(input.month).endOf("month").add(6, "days");
return await integrationInstance.getCalendarEventsAsync(
startDate.toDate(),
endDate.toDate(),
+3 -3
View File
@@ -9,11 +9,11 @@ import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-
export const downloadClientRequestHandler = createCachedIntegrationRequestHandler<
DownloadClientJobsAndStatus,
IntegrationKindByCategory<"downloadClient">,
Record<string, never>
{ limit: number }
>({
async requestAsync(integration, _input) {
async requestAsync(integration, input) {
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getClientJobsAndStatusAsync();
return await integrationInstance.getClientJobsAndStatusAsync(input);
},
cacheDuration: dayjs.duration(5, "seconds"),
queryKey: "downloadClientJobStatus",
+1 -1
View File
@@ -26,7 +26,7 @@
"@homarr/api": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@mantine/dates": "^8.0.1",
"@mantine/dates": "^8.0.2",
"next": "15.3.2",
"react": "19.1.0",
"react-dom": "19.1.0"
+4 -4
View File
@@ -33,11 +33,11 @@
"@homarr/settings": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^8.0.1",
"@mantine/hooks": "^8.0.1",
"@mantine/spotlight": "^8.0.1",
"@mantine/core": "^8.0.2",
"@mantine/hooks": "^8.0.2",
"@mantine/spotlight": "^8.0.2",
"@tabler/icons-react": "^3.33.0",
"jotai": "^2.12.4",
"jotai": "^2.12.5",
"next": "15.3.2",
"react": "19.1.0",
"react-dom": "19.1.0",
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": ""
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": ""
},
"defaultTab": {
"label": ""
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": ""
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "",
"search": "",
"selected": ""
"selected": "",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": ""
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": ""
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "显示内存信息"
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": "显示文件系统信息"
},
"defaultTab": {
"label": "默认标签"
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": "部分指标要求"
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "城市/邮编",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": "使用过滤器来计算比率"
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": "网络控制器"
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "已更新 {when}",
"search": "搜索 {count} 容器",
"selected": "{totalCount} 容器的 {selectCount}"
"selected": "{totalCount} 容器的 {selectCount}",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": "废弃"
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "镜像"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "开始",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "Zobrazit informace o paměti"
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": "Zobrazit informace o souborovém systému"
},
"defaultTab": {
"label": ""
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": ""
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "",
"search": "",
"selected": ""
"selected": "",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": ""
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "Obraz"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "Spustit",
"notification": {
+156 -125
View File
@@ -705,125 +705,125 @@
"error": {
"common": {
"cause": {
"title": ""
"title": "Årsag med flere detaljer"
}
},
"unknown": {
"title": "",
"description": ""
"title": "Ukendt fejl",
"description": "En ukendt fejl opstod, åbn årsagen nedenfor for at se flere detaljer"
},
"parse": {
"title": "",
"description": ""
"title": "Parsingfejl",
"description": "Svaret kunne ikke fortolkes. Kontroller, at URL'en peger på tjenestens grundlæggende URL."
},
"authorization": {
"title": "",
"description": ""
"title": "Godkendelsesfejl",
"description": "Anmodningen blev ikke godkendt. Kontroller, at legitimationsoplysningerne er korrekte og at du har dem konfigureret med nok tilladelser."
},
"statusCode": {
"title": "",
"description": "",
"otherDescription": "",
"title": "Svar fejl",
"description": "Modtaget uventet {statusCode} ({reason}) svar fra <url></url>. Kontroller, at URL'en peger på basis-URL'en for integrationen.",
"otherDescription": "Modtaget uventet {statusCode} svar fra <url></url>. Kontroller, at URLen peger på den grundlæggende URL for integrationen.",
"reason": {
"badRequest": "",
"notFound": "",
"tooManyRequests": "",
"internalServerError": "",
"serviceUnavailable": "",
"gatewayTimeout": ""
"badRequest": "Ugyldig forespørgsel",
"notFound": "Ikke fundet",
"tooManyRequests": "For mange forespørgsler",
"internalServerError": "Intern serverfejl",
"serviceUnavailable": "Tjeneste ikke tilgængelig",
"gatewayTimeout": "Gateway-timeout"
}
},
"certificate": {
"title": "",
"title": "Certifikatfejl",
"description": {
"expired": "",
"notYetValid": "",
"untrusted": "",
"hostnameMismatch": ""
"expired": "Certifikatet er udløbet.",
"notYetValid": "Certifikatet er ikke gyldigt endnu.",
"untrusted": "Der er ikke tillid til certifikatet.",
"hostnameMismatch": "Certifikatets værtsnavn matcher ikke URL'en."
},
"alert": {
"permission": {
"title": "",
"message": ""
"title": "Ikke tilstrækkelige tilladelser",
"message": "Du har ikke tilladelse til at stole på eller uploade certifikater. Kontakt venligst din administrator for at uploade det nødvendige rodcertifikat."
},
"hostnameMismatch": {
"title": "",
"message": ""
"title": "Værtsnavn matcher ikke",
"message": "Værtsnavnet i certifikatet matcher ikke det værtsnavn du forbinder til. Dette kan indikere en sikkerhedsrisiko, men du kan stadig vælge at stole på dette certifikat."
},
"extract": {
"title": "",
"message": ""
"title": "Ekstraktion af CA-certifikat mislykkedes",
"message": "Kun selvsignerede certifikater uden kæde kan hentes automatisk. Hvis du bruger et selvsigneret certifikat, skal du sørge for at uploade CA-certifikatet manuelt. Du kan finde instruktioner om, hvordan du gør dette <docsLink></docsLink>."
}
},
"action": {
"retry": {
"label": ""
"label": "Gentag oprettelse"
},
"trust": {
"label": ""
"label": "Stol på certifikat"
},
"upload": {
"label": ""
"label": "Upload certifikat"
}
},
"hostnameMismatch": {
"confirm": {
"title": "",
"message": ""
"title": "Stol på uoverensstemmelse mellem værtsnavn",
"message": "Er du sikker på du vil have tillid til certifikatet med et værtsnavn uoverensstemmelse?"
},
"notification": {
"success": {
"title": "",
"message": ""
"title": "Betroet certifikat",
"message": "Tilføjet værtsnavn til liste over betroede certifikater"
},
"error": {
"title": "",
"message": ""
"title": "Kunne ikke stole på certifikat",
"message": "Certifikatet med et værtsnavn mismatch kunne ikke stoles på"
}
}
},
"selfSigned": {
"confirm": {
"title": "",
"message": ""
"title": "Stol på selvsigneret certifikat",
"message": "Er du sikker på, at du vil stole på dette selvsignerede certifikat?"
},
"notification": {
"success": {
"title": "",
"message": ""
"title": "Betroet certifikat",
"message": "Tilføjet certifikat til liste over betroede certifikater"
},
"error": {
"title": "",
"message": ""
"title": "Kunne ikke stole på certifikat",
"message": "Kunne ikke tilføje certifikat til listen over betroede certifikater"
}
}
},
"details": {
"title": "",
"description": "",
"title": "Detaljer",
"description": "Gennemgå certifikatoplysningerne, før du beslutter dig for at stole på den.",
"content": {
"action": "",
"title": ""
"action": "Vis indhold",
"title": "PEM Certifikat"
}
}
},
"request": {
"title": "",
"title": "Anmodningsfejl",
"description": {
"connection": {
"hostUnreachable": "",
"networkUnreachable": "",
"refused": "",
"reset": ""
"hostUnreachable": "Serveren kunne ikke nås. Dette betyder normalt, at værten er offline eller ikke kan nås fra dit netværk.",
"networkUnreachable": "Netværket er ikke tilgængeligt. Tjek venligst din internetforbindelse eller netværkskonfiguration.",
"refused": "Serveren nægtede forbindelsen. Den kører muligvis ikke eller afviser forespørgsler på den angivne port.",
"reset": "Forbindelsen blev uventet lukket af serveren. Dette kan ske hvis serveren er ustabil eller genstartet."
},
"dns": {
"notFound": "",
"timeout": "",
"noAnswer": ""
"notFound": "Serveradressen kunne ikke findes. Tjek venligst URL'en for stave eller ugyldige domænenavne.",
"timeout": "DNS-opslag fik timeout. Dette kan være et midlertidigt problem - prøv igen om et øjeblik.",
"noAnswer": "DNS-serveren returnerede ikke et gyldigt svar. Domænet findes måske men har ingen gyldige poster."
},
"timeout": {
"aborted": "",
"timeout": ""
"aborted": "Anmodningen blev afbrudt før den kunne fuldføres. Dette kan skyldes en bruger handling eller system timeout.",
"timeout": "Forespørgslen tog for lang tid at fuldføre og fik timeout. Tjek dit netværk eller prøv igen senere."
}
}
}
@@ -1004,7 +1004,7 @@
"cancel": "Annuller",
"delete": "Slet",
"discard": "Kassér",
"close": "",
"close": "Luk",
"confirm": "Bekræft",
"continue": "Forsæt",
"previous": "Forrige",
@@ -1220,7 +1220,7 @@
"label": "Integrationer"
},
"title": {
"label": ""
"label": "Titel"
},
"customCssClasses": {
"label": "Brugerdefinerede CSS-klasser"
@@ -1757,12 +1757,18 @@
"memory": {
"label": "Vis hukommelsesoplysninger"
},
"showUptime": {
"label": "Vis Oppetid"
},
"fileSystem": {
"label": "Vis information om filsystemet"
},
"defaultTab": {
"label": "Standard fane"
},
"visibleClusterSections": {
"label": "Synlige klynge sektioner"
},
"sectionIndicatorRequirement": {
"label": "Sektion indikator er et krav"
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "Docker statistik",
"description": "Statistik for dine containere (Denne widget kan kun tilføjes med administratorrettigheder)",
"option": {},
"error": {
"internalServerError": "Kunne ikke hente containerstatistik"
}
},
"common": {
"location": {
"query": "By / Postnummer",
@@ -1898,8 +1912,8 @@
"description": "Vis de aktuelle streams på dine medieservere",
"option": {
"showOnlyPlaying": {
"label": "",
"description": ""
"label": "Vis kun hvad der aktuelt afspilles",
"description": "Deaktivering af dette vil ikke virke for plex"
}
},
"items": {
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": "Brug filteret til at beregne forholdet"
},
"limitPerIntegration": {
"label": "Begræns elementer pr. integration",
"description": "Dette vil begrænse antallet af elementer vist pr. integration, ikke globalt"
}
},
"errors": {
@@ -2077,7 +2095,7 @@
"approved": "Godkendt",
"declined": "Afvist",
"failed": "Mislykket",
"completed": ""
"completed": "Fuldført"
},
"toBeDetermined": "TBD"
},
@@ -2181,94 +2199,94 @@
}
},
"releases": {
"name": "",
"description": "",
"name": "Udgivelser",
"description": "Viser en liste over den aktuelle version af de givne depoter med den givne version regex.",
"option": {
"newReleaseWithin": {
"label": "",
"description": ""
"label": "Ny Udgivelse Inden",
"description": "Anvendelseseksempel: 1w (1 uge), 10M (10 måneder). Accepterede enhedstyper h (timer), d (dage), w (uger), M (måneder), y (år). Lad være tom for ingen fremhævning af nye udgivelser."
},
"staleReleaseWithin": {
"label": "",
"description": ""
"label": "Gamle Udgivelse Indenfor",
"description": "Anvendelseseksempel: 1w (1 uge), 10M (10 måneder). Accepterede enhedstyper h (timer), d (dage), w (uger), M (måneder), y (år). Lad være tom for ingen fremhævning af forældede udgivelser."
},
"showOnlyHighlighted": {
"label": "",
"description": ""
"label": "Vis Kun Fremhævede",
"description": "Vis kun nye eller forældede udgivelser. Som pr ovenfor."
},
"showDetails": {
"label": ""
"label": "Vis detaljer"
},
"topReleases": {
"label": "",
"description": ""
"label": "Top Udgivelser",
"description": "Det maksimale antal seneste udgivelser at vise. Nul betyder ingen grænse."
},
"repositories": {
"label": "",
"label": "Repositories",
"addRRepository": {
"label": ""
"label": "Tilføj repository"
},
"provider": {
"label": ""
"label": "Udbyder"
},
"identifier": {
"label": "",
"placeholder": ""
"label": "Identifikator",
"placeholder": "Navn eller ejer/navn"
},
"name": {
"label": ""
"label": "Navn"
},
"versionFilter": {
"label": "",
"label": "Versionsfilter",
"prefix": {
"label": ""
"label": "Præfiks"
},
"precision": {
"label": "",
"label": "Præcision",
"options": {
"none": ""
"none": "Ingen"
}
},
"suffix": {
"label": ""
"label": "Suffiks"
},
"regex": {
"label": ""
"label": "Regulært Udtryk"
}
},
"edit": {
"label": ""
"label": "Rediger"
},
"editForm": {
"title": "",
"title": "Rediger Repository",
"cancel": {
"label": ""
"label": "Annuller"
},
"confirm": {
"label": ""
"label": "Bekræft"
}
},
"example": {
"label": ""
"label": "Eksempel"
},
"invalid": ""
"invalid": "Ugyldig repository definition, tjek venligst værdierne"
}
},
"not-found": "",
"pre-release": "",
"archived": "",
"forked": "",
"starsCount": "",
"forksCount": "",
"issuesCount": "",
"openProjectPage": "",
"openReleasePage": "",
"releaseDescription": "",
"created": "",
"not-found": "Ikke fundet",
"pre-release": "Pre-Release",
"archived": "Arkiveret",
"forked": "Forked",
"starsCount": "Stjerner",
"forksCount": "Forks",
"issuesCount": "Åbne Problemer",
"openProjectPage": "Åbn Projektside",
"openReleasePage": "Åbn Udgivelsesside",
"releaseDescription": "Udgivelse Beskrivelse",
"created": "Oprettet",
"error": {
"label": "",
"label": "Fejl",
"options": {
"noMatchingVersion": ""
"noMatchingVersion": "Ingen matchende version fundet"
}
}
},
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": "Netværkskontroller"
},
"dockerContainers": {
"label": "Docker containers"
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "Opdateret {when}",
"search": "Søg {count} containere",
"selected": "{selectCount} af {totalCount} containere valgt"
"selected": "{selectCount} af {totalCount} containere valgt",
"footer": "Total {count} containere"
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": "Død"
}
},
"stats": {
"cpu": {
"label": "CPU"
},
"memory": {
"label": "Hukommelse"
}
},
"containerImage": {
"label": "Billede"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "Handlinger",
"start": {
"label": "Start",
"notification": {
@@ -3625,7 +3656,7 @@
"certificates": {
"label": "Certifikater",
"hostnames": {
"label": ""
"label": "Værtsnavne"
}
}
},
@@ -4026,25 +4057,25 @@
"certificate": {
"field": {
"hostname": {
"label": ""
"label": "Værtsnavn"
},
"subject": {
"label": ""
"label": "Emne"
},
"issuer": {
"label": ""
"label": "Udsteder"
},
"validFrom": {
"label": ""
"label": "Gyldig fra"
},
"validTo": {
"label": ""
"label": "Gyldig til"
},
"serialNumber": {
"label": ""
"label": "Serienummer"
},
"fingerprint": {
"label": ""
"label": "Fingeraftryk"
}
},
"page": {
@@ -4055,19 +4086,19 @@
"title": "Der er endnu ingen certifikater"
},
"invalid": {
"title": "",
"description": ""
"title": "Ugyldigt certifikat",
"description": "Kunne ikke fortolke certifikat"
},
"expires": "Udløber {when}",
"toHostnames": ""
"toHostnames": "Betroede værtsnavne"
},
"hostnames": {
"title": "",
"description": "",
"title": "Betroet certifikat værtsnavne",
"description": "Nogle certifikater tillader ikke det specifikke domæne Homarr bruger til at anmode om dem, på grund af dette alle betroede værtsnavne med deres certifikat thumbprints bruges til at omgå disse restriktioner.",
"noResults": {
"title": ""
"title": "Der er endnu ingen værtsnavne"
},
"toCertificates": ""
"toCertificates": "Certifikater"
}
},
"action": {
@@ -4099,16 +4130,16 @@
}
},
"removeHostname": {
"label": "",
"confirm": "",
"label": "Fjern betroet værtsnavn",
"confirm": "Er du sikker på du vil fjerne dette betroede værtsnavn? Dette kan få nogle integrationer til at stoppe med at virke.",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Værtsnavn fjernet",
"message": "Værtsnavnet blev fjernet korrekt"
},
"error": {
"title": "",
"message": ""
"title": "Værtsnavn ikke fjernet",
"message": "Værtsnavnet kunne ikke fjernes"
}
}
}
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "Speicher-Info anzeigen"
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": "Dateisystem Info anzeigen"
},
"defaultTab": {
"label": "Standard Tab"
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": "Anforderung der Sektionsindikatoren"
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "Stadt / Postleitzahl",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": "Filter zur Berechnung des Verhältnisses verwenden"
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "Aktualisiert {when}",
"search": "{count} Container durchsuchen",
"selected": "{selectCount} von {totalCount} ausgewählten Containern"
"selected": "{selectCount} von {totalCount} ausgewählten Containern",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": "Tot"
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": ""
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "Starten",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "Speicher-Info anzeigen"
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": "Dateisystem Info anzeigen"
},
"defaultTab": {
"label": "Standard Tab"
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": "Anforderung der Sektionsindikatoren"
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "Stadt / Postleitzahl",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": "Filter zur Berechnung des Verhältnisses verwenden"
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": "Netzwerk Controller"
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "Aktualisiert {when}",
"search": "{count} Container durchsuchen",
"selected": "{selectCount} von {totalCount} ausgewählten Containern"
"selected": "{selectCount} von {totalCount} ausgewählten Containern",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": "Tot"
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "Image"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "Starten",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "Εμφάνιση Πληροφοριών Μνήμης"
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": "Εμφάνιση Πληροφοριών Συστήματος Αρχείων"
},
"defaultTab": {
"label": ""
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": ""
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "",
"search": "",
"selected": ""
"selected": "",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": ""
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "Εικόνα"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "Έναρξη",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": ""
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": ""
},
"defaultTab": {
"label": ""
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": ""
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "",
"search": "",
"selected": ""
"selected": "",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": ""
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": ""
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "",
"notification": {
+10
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "Show Memory Info"
},
"showUptime": {
"label": "Show Uptime"
},
"fileSystem": {
"label": "Show Filesystem Info"
},
"defaultTab": {
"label": "Default tab"
},
"visibleClusterSections": {
"label": "Visible cluster sections"
},
"sectionIndicatorRequirement": {
"label": "Section indicator requirement"
}
@@ -1953,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": "Use filter to calculate Ratio"
},
"limitPerIntegration": {
"label": "Limit items per integration",
"description": "This will limit the number of items shown per integration, not globally"
}
},
"errors": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "Mostrar información de la memoria"
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": "Mostrar información del sistema de archivos"
},
"defaultTab": {
"label": ""
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": ""
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "",
"search": "",
"selected": ""
"selected": "",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": ""
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "Imagen"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "Iniciar",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": ""
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": ""
},
"defaultTab": {
"label": ""
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": ""
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "",
"search": "",
"selected": ""
"selected": "",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": ""
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": ""
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "Afficher les infos de la mémoire"
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": "Afficher les infos sur le système de fichiers"
},
"defaultTab": {
"label": "Onglet par défaut"
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": "Exigence de l'indicateur de section"
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "Ville / Code Postal",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": "Utiliser le filtre pour calculer le ratio"
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "Mis à jour {when}",
"search": "Rechercher dans {count} conteneurs",
"selected": "{selectCount} sur {totalCount} conteneurs sélectionnés"
"selected": "{selectCount} sur {totalCount} conteneurs sélectionnés",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": "Mort"
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "Image"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "Début",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "הצג מידע זיכרון"
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": "הצג מידע על מערכת הקבצים"
},
"defaultTab": {
"label": "כרטיסיית ברירת מחדל"
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": "דרישת מציין מקטע"
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "סטטיסטיקות דוקר",
"description": "סטטיסטיקות של המכולות שלך (ניתן להוסיף ווידג'ט זה רק עם הרשאות מנהל)",
"option": {},
"error": {
"internalServerError": "נכשל באחזור סטטיסטיקות המכולות"
}
},
"common": {
"location": {
"query": "עיר / מיקוד",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": "השתמש במסנן כדי לחשב יחס"
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": "בקר רשת"
},
"dockerContainers": {
"label": "מכולות דוקר"
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "עודכן ב- {when}",
"search": "חפש {count} קונטיינרים",
"selected": "{selectCount} מ- {totalCount} קונטיינרים נבחרו"
"selected": "{selectCount} מ- {totalCount} קונטיינרים נבחרו",
"footer": "סה״כ {count} קונטיינרים"
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": "מת"
}
},
"stats": {
"cpu": {
"label": "מעבד"
},
"memory": {
"label": "זיכרון"
}
},
"containerImage": {
"label": "קובץ תמונה"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "פעולות",
"start": {
"label": "התחל",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": ""
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": ""
},
"defaultTab": {
"label": ""
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": ""
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "",
"search": "",
"selected": ""
"selected": "",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": ""
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "Slika"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "Pokreni",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "Memóriainformációk megjelenítése"
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": "Fájlrendszer-információk megjelenítése"
},
"defaultTab": {
"label": ""
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": ""
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "",
"search": "",
"selected": ""
"selected": "",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": ""
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "Kép"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "Indítás",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "Mostra Informazioni Memoria"
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": "Mostra Informazioni Filesystem"
},
"defaultTab": {
"label": ""
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": ""
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "",
"search": "",
"selected": ""
"selected": "",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": ""
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "Immagine"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "Avvia",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "メモリー情報を表示"
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": "ファイルシステム情報を表示"
},
"defaultTab": {
"label": ""
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": ""
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "",
"search": "",
"selected": ""
"selected": "",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": ""
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "画像"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "開始",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": ""
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": ""
},
"defaultTab": {
"label": ""
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": ""
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "",
"search": "",
"selected": ""
"selected": "",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": ""
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "이미지"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "시작",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": ""
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": ""
},
"defaultTab": {
"label": ""
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": ""
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "",
"search": "",
"selected": ""
"selected": "",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": ""
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "Nuotrauka"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "Paleisti",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "Rādīt atmiņas informāciju"
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": "Rādīt failu sistēmas informāciju"
},
"defaultTab": {
"label": ""
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": ""
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "",
"search": "",
"selected": ""
"selected": "",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": ""
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "Attēls"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "Palaist",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "Werkgeheugen info weergeven"
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": "Bestandssysteem info weergeven"
},
"defaultTab": {
"label": "Standaard tab"
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": "Sectie indicator vereiste"
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "Stad / postcode",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": "Gebruik filter om ratio te berekenen"
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": "Netwerkcontroller"
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "{when} bijgewerkt",
"search": "Zoek {count} containers",
"selected": "{selectCount} van {totalCount} containers geselecteerd"
"selected": "{selectCount} van {totalCount} containers geselecteerd",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": "Dood"
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "Image"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "Starten",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "Vis minneinfo"
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": "Vis filsysteminfo"
},
"defaultTab": {
"label": "Standard fane"
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": "Krav til seksjonsindikator"
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "By / Postnummer",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": "Bruk filter for å beregne ratio"
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "Oppdatert {when}",
"search": "Søk i {count} containere",
"selected": "{selectCount} av {totalCount} containere valgt"
"selected": "{selectCount} av {totalCount} containere valgt",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": "Død"
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "Image"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "Start",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "Pokaż informacje o pamięci"
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": "Pokaż informacje o systemie plików"
},
"defaultTab": {
"label": ""
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "Miasto / kod pocztowy",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": "Użyj filtra do obliczenia współczynnika"
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "",
"search": "",
"selected": ""
"selected": "",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": ""
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "Obraz"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "Uruchom",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "Mostrar informações da memória"
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": "Mostrar informações do sistema de arquivos"
},
"defaultTab": {
"label": ""
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": ""
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "",
"search": "",
"selected": ""
"selected": "",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": ""
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "Imagem"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "Iniciar",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "Afișare informații memorie"
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": "Afișare informații despre sistemul de fișiere"
},
"defaultTab": {
"label": ""
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": ""
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "",
"search": "",
"selected": ""
"selected": "",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": ""
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "Imagine"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "Pornește",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "Показать информацию о памяти"
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": "Показать информацию о файловой системе"
},
"defaultTab": {
"label": "Вкладка по умолчанию"
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": "Пороговое значение индикатора раздела"
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "Город / Почтовый индекс",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": "Использовать фильтр при расчёте рейтинга"
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "Обновлено {when}",
"search": "Поиск среди {count} контейнеров",
"selected": "Выбрано {selectCount} из {totalCount} контейнеров"
"selected": "Выбрано {selectCount} из {totalCount} контейнеров",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": "Нерабочий"
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "Образ"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "Запустить",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "Zobraziť informácie o pamäti"
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": "Zobraziť informácie o súborovom systéme"
},
"defaultTab": {
"label": "Predvolená karta"
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "Mesto / PSČ",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": "Na výpočet pomeru použite filter"
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "Aktualizované {when}",
"search": "Vyhľadajte {count} kontajnery",
"selected": "{selectCount} z {totalCount} vybratých kontajnerov"
"selected": "{selectCount} z {totalCount} vybratých kontajnerov",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": "Mŕtvy"
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "Obraz"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "Spustiť",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": ""
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": ""
},
"defaultTab": {
"label": ""
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": ""
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "",
"search": "",
"selected": ""
"selected": "",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": ""
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "Slika"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "Zaženi",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "Visa minnesinformation"
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": "Visa information om filsystemet"
},
"defaultTab": {
"label": ""
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": ""
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "",
"search": "",
"selected": ""
"selected": "",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": ""
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": ""
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "Starta",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "Bellek Bilgilerini Göster"
},
"showUptime": {
"label": "Çalışma Süresini Göster"
},
"fileSystem": {
"label": "Dosya Sistemi Bilgilerini Göster"
},
"defaultTab": {
"label": "Varsayılan sekme"
},
"visibleClusterSections": {
"label": "Görünür küme alanları"
},
"sectionIndicatorRequirement": {
"label": "Bölüm göstergesi gereksinimi"
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "Docker istatistikleri",
"description": "Konteynerlerinizin istatistikleri (Bu widget yalnızca yönetici ayrıcalıklarıyla eklenebilir)",
"option": {},
"error": {
"internalServerError": "Konteyner istatistikleri alınamadı"
}
},
"common": {
"location": {
"query": "Şehir / Posta kodu",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": "Oranı hesaplamak için filtreyi kullanın"
},
"limitPerIntegration": {
"label": "Her entegrasyon için öğe sınırı",
"description": "Bu, gösterilecek öğe sayısını tüm entegrasyonlar için değil, her entegrasyon özelinde sınırlar"
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": "Ağ Denetleyicisi"
},
"dockerContainers": {
"label": "Docker konteynerleri"
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "{when} Güncellendi",
"search": "{count} konteyner içinde ara",
"selected": "{selectCount} / {totalCount} konteyner seçildi"
"selected": "{selectCount} / {totalCount} konteyner seçildi",
"footer": "Toplam {count} konteyner"
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": "Ölü"
}
},
"stats": {
"cpu": {
"label": "İşlemci"
},
"memory": {
"label": "Bellek"
}
},
"containerImage": {
"label": "İmaj"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "Eylemler",
"start": {
"label": "Başlat",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "Показати інформацію про пам'ять"
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": ""
},
"defaultTab": {
"label": ""
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "Місто / Поштовий індекс",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": "Використати фільтр при розрахунку рейтингу"
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "Оновлено {when}",
"search": "",
"selected": "Вибрано {selectCount} із {totalCount} контейнерів"
"selected": "Вибрано {selectCount} із {totalCount} контейнерів",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": "Несправний"
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "Образ"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "Пуск",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": ""
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": ""
},
"defaultTab": {
"label": ""
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": ""
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": ""
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": ""
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "",
"search": "",
"selected": ""
"selected": "",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": ""
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "Hình ảnh"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "Bắt đầu",
"notification": {
+32 -1
View File
@@ -1757,12 +1757,18 @@
"memory": {
"label": "顯示記憶體訊息"
},
"showUptime": {
"label": ""
},
"fileSystem": {
"label": "顯示檔案系統訊息"
},
"defaultTab": {
"label": "預設頁面"
},
"visibleClusterSections": {
"label": ""
},
"sectionIndicatorRequirement": {
"label": "部分指示需求"
}
@@ -1832,6 +1838,14 @@
}
}
},
"dockerContainers": {
"name": "",
"description": "",
"option": {},
"error": {
"internalServerError": ""
}
},
"common": {
"location": {
"query": "城市 / 郵遞區號",
@@ -1945,6 +1959,10 @@
},
"applyFilterToRatio": {
"label": "使用篩選器來計算速率"
},
"limitPerIntegration": {
"label": "",
"description": ""
}
},
"errors": {
@@ -3091,6 +3109,9 @@
},
"networkController": {
"label": "網路控制"
},
"dockerContainers": {
"label": ""
}
}
},
@@ -3155,7 +3176,8 @@
"table": {
"updated": "已更新於 {when}",
"search": "搜尋 {count} 容器",
"selected": "{totalCount} 容器的 {selectCount}"
"selected": "{totalCount} 容器的 {selectCount}",
"footer": ""
},
"field": {
"name": {
@@ -3173,6 +3195,14 @@
"dead": "廢棄"
}
},
"stats": {
"cpu": {
"label": ""
},
"memory": {
"label": ""
}
},
"containerImage": {
"label": "鏡像"
},
@@ -3181,6 +3211,7 @@
}
},
"action": {
"title": "",
"start": {
"label": "啟動",
"notification": {
+3 -3
View File
@@ -29,9 +29,9 @@
"@homarr/log": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.0.1",
"@mantine/dates": "^8.0.1",
"@mantine/hooks": "^8.0.1",
"@mantine/core": "^8.0.2",
"@mantine/dates": "^8.0.2",
"@mantine/hooks": "^8.0.2",
"@tabler/icons-react": "^3.33.0",
"mantine-react-table": "2.0.0-beta.9",
"next": "15.3.2",
+1 -1
View File
@@ -24,7 +24,7 @@
"dependencies": {
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"zod": "^3.25.23",
"zod": "^3.25.30",
"zod-form-data": "^2.0.7"
},
"devDependencies": {
+1 -1
View File
@@ -4,7 +4,7 @@ export const appHrefSchema = z
.string()
.trim()
.url()
.regex(/^https?:\/\//) // Only allow http and https for security reasons (javascript: is not allowed)
.regex(/^(?!javascript)[a-zA-Z]*:\/\//i) // javascript: is not allowed, i for case insensitive (so Javascript: is also not allowed)
.or(z.literal(""))
.transform((value) => (value.length === 0 ? null : value))
.nullable();
+4 -4
View File
@@ -48,9 +48,9 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/charts": "^8.0.1",
"@mantine/core": "^8.0.1",
"@mantine/hooks": "^8.0.1",
"@mantine/charts": "^8.0.2",
"@mantine/core": "^8.0.2",
"@mantine/hooks": "^8.0.2",
"@tabler/icons-react": "^3.33.0",
"@tiptap/extension-color": "2.12.0",
"@tiptap/extension-highlight": "2.12.0",
@@ -76,7 +76,7 @@
"react-markdown": "^10.1.0",
"recharts": "^2.15.3",
"video.js": "^8.22.0",
"zod": "^3.25.23"
"zod": "^3.25.30"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
@@ -44,8 +44,8 @@ export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: C
h="100%"
w="100%"
p={0}
pt={isSmall ? 5 : 20}
pb={isSmall ? 5 : 20}
pt={isSmall ? 0 : 10}
pb={isSmall ? 0 : 10}
m={0}
bd={`2px solid ${opened && !disabled ? primaryColor : "transparent"}`}
style={{
+11 -4
View File
@@ -68,6 +68,7 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
const mantineTheme = useMantineTheme();
const actualItemRadius = mantineTheme.radius[board.itemRadius];
const { ref, width, height } = useElementSize();
const isSmall = width < 256;
return (
<Calendar
@@ -82,16 +83,20 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
firstDayOfWeek={firstDayOfWeek}
static={isEditMode}
className={classes.calendar}
w={"100%"}
h={"100%"}
w="100%"
h="100%"
ref={ref}
styles={{
calendarHeaderControl: {
pointerEvents: isEditMode ? "none" : undefined,
borderRadius: "md",
height: isSmall ? "1.5rem" : undefined,
width: isSmall ? "1.5rem" : undefined,
},
calendarHeaderLevel: {
pointerEvents: "none",
fontSize: isSmall ? "0.75rem" : undefined,
height: "100%",
},
levelsGroup: {
height: "100%",
@@ -107,10 +112,12 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
day: {
borderRadius: actualItemRadius,
width: "100%",
height: "auto",
height: "100%",
position: "relative",
},
month: {},
month: {
height: "100%",
},
weekday: {
padding: 0,
},
+3 -1
View File
@@ -87,6 +87,7 @@ export default function DownloadClientsWidget({
const [currentItems] = clientApi.widget.downloads.getJobsAndStatuses.useSuspenseQuery(
{
integrationIds,
limitPerIntegration: options.limitPerIntegration,
},
{
refetchOnMount: false,
@@ -126,6 +127,7 @@ export default function DownloadClientsWidget({
clientApi.widget.downloads.subscribeToJobsAndStatuses.useSubscription(
{
integrationIds,
limitPerIntegration: options.limitPerIntegration,
},
{
onData: (data) => {
@@ -536,7 +538,7 @@ export default function DownloadClientsWidget({
sortUndefined: "last",
Cell: ({ cell }) => {
const upSpeed = cell.getValue<ExtendedDownloadClientItem["upSpeed"]>();
return upSpeed && <Text>{humanFileSize(upSpeed, "/s")}</Text>;
return upSpeed && <Text size="xs">{humanFileSize(upSpeed, "/s")}</Text>;
},
},
],
+5
View File
@@ -82,6 +82,11 @@ export const { definition, componentLoader } = createWidgetDefinition("downloads
applyFilterToRatio: factory.switch({
defaultValue: true,
}),
limitPerIntegration: factory.number({
defaultValue: 50,
validate: z.number().min(1),
withDescription: true,
}),
}),
{
defaultSort: {
@@ -72,126 +72,156 @@ export const ClusterHealthMonitoring = ({
const cpuPercent = maxCpu ? (usedCpu / maxCpu) * 100 : 0;
const memPercent = maxMem ? (usedMem / maxMem) * 100 : 0;
const defaultValue = [options.visibleClusterSections.at(0) ?? "node"];
const isTiny = width < 256;
return (
<Stack h="100%" p="xs" gap={isTiny ? "xs" : "md"}>
<Group justify="center" wrap="nowrap">
<Text fz={isTiny ? 8 : "xs"} tt="uppercase" fw={700} c="dimmed" ta="center">
{formatUptime(uptime, t)}
</Text>
</Group>
<SummaryHeader cpu={cpuPercent} memory={memPercent} isTiny={isTiny} />
<Accordion variant="contained" chevronPosition="right" multiple defaultValue={["node"]}>
<ResourceAccordionItem
value="node"
title={t("widget.healthMonitoring.cluster.resource.node.name")}
icon={IconServer}
badge={addBadgeColor({
activeCount: activeNodes,
totalCount: healthData.nodes.length,
sectionIndicatorRequirement: options.sectionIndicatorRequirement,
})}
isTiny={isTiny}
>
<ResourceTable type="node" data={healthData.nodes} isTiny={isTiny} />
</ResourceAccordionItem>
{options.showUptime && (
<Group justify="center" wrap="nowrap">
<Text fz={isTiny ? 8 : "xs"} tt="uppercase" fw={700} c="dimmed" ta="center">
{formatUptime(uptime, t)}
</Text>
</Group>
)}
<SummaryHeader
cpu={{
value: cpuPercent,
hidden: !options.cpu,
}}
memory={{
value: memPercent,
hidden: !options.memory,
}}
isTiny={isTiny}
/>
{options.visibleClusterSections.length >= 1 && (
<Accordion variant="contained" chevronPosition="right" multiple defaultValue={defaultValue}>
{options.visibleClusterSections.includes("node") && (
<ResourceAccordionItem
value="node"
title={t("widget.healthMonitoring.cluster.resource.node.name")}
icon={IconServer}
badge={addBadgeColor({
activeCount: activeNodes,
totalCount: healthData.nodes.length,
sectionIndicatorRequirement: options.sectionIndicatorRequirement,
})}
isTiny={isTiny}
>
<ResourceTable type="node" data={healthData.nodes} isTiny={isTiny} />
</ResourceAccordionItem>
)}
<ResourceAccordionItem
value="qemu"
title={t("widget.healthMonitoring.cluster.resource.qemu.name")}
icon={IconDeviceLaptop}
badge={addBadgeColor({
activeCount: activeVMs,
totalCount: healthData.vms.length,
sectionIndicatorRequirement: options.sectionIndicatorRequirement,
})}
isTiny={isTiny}
>
<ResourceTable type="qemu" data={healthData.vms} isTiny={isTiny} />
</ResourceAccordionItem>
{options.visibleClusterSections.includes("qemu") && (
<ResourceAccordionItem
value="qemu"
title={t("widget.healthMonitoring.cluster.resource.qemu.name")}
icon={IconDeviceLaptop}
badge={addBadgeColor({
activeCount: activeVMs,
totalCount: healthData.vms.length,
sectionIndicatorRequirement: options.sectionIndicatorRequirement,
})}
isTiny={isTiny}
>
<ResourceTable type="qemu" data={healthData.vms} isTiny={isTiny} />
</ResourceAccordionItem>
)}
<ResourceAccordionItem
value="lxc"
title={t("widget.healthMonitoring.cluster.resource.lxc.name")}
icon={IconCube}
badge={addBadgeColor({
activeCount: activeLXCs,
totalCount: healthData.lxcs.length,
sectionIndicatorRequirement: options.sectionIndicatorRequirement,
})}
isTiny={isTiny}
>
<ResourceTable type="lxc" data={healthData.lxcs} isTiny={isTiny} />
</ResourceAccordionItem>
{options.visibleClusterSections.includes("lxc") && (
<ResourceAccordionItem
value="lxc"
title={t("widget.healthMonitoring.cluster.resource.lxc.name")}
icon={IconCube}
badge={addBadgeColor({
activeCount: activeLXCs,
totalCount: healthData.lxcs.length,
sectionIndicatorRequirement: options.sectionIndicatorRequirement,
})}
isTiny={isTiny}
>
<ResourceTable type="lxc" data={healthData.lxcs} isTiny={isTiny} />
</ResourceAccordionItem>
)}
<ResourceAccordionItem
value="storage"
title={t("widget.healthMonitoring.cluster.resource.storage.name")}
icon={IconDatabase}
badge={addBadgeColor({
activeCount: activeStorage,
totalCount: healthData.storages.length,
sectionIndicatorRequirement: options.sectionIndicatorRequirement,
})}
isTiny={isTiny}
>
<ResourceTable type="storage" data={healthData.storages} isTiny={isTiny} />
</ResourceAccordionItem>
</Accordion>
{options.visibleClusterSections.includes("storage") && (
<ResourceAccordionItem
value="storage"
title={t("widget.healthMonitoring.cluster.resource.storage.name")}
icon={IconDatabase}
badge={addBadgeColor({
activeCount: activeStorage,
totalCount: healthData.storages.length,
sectionIndicatorRequirement: options.sectionIndicatorRequirement,
})}
isTiny={isTiny}
>
<ResourceTable type="storage" data={healthData.storages} isTiny={isTiny} />
</ResourceAccordionItem>
)}
</Accordion>
)}
</Stack>
);
};
interface SummaryHeaderProps {
cpu: number;
memory: number;
cpu: { value: number; hidden: boolean };
memory: { value: number; hidden: boolean };
isTiny: boolean;
}
const SummaryHeader = ({ cpu, memory, isTiny }: SummaryHeaderProps) => {
const t = useI18n();
if (cpu.hidden && memory.hidden) return null;
return (
<Center>
<Group wrap="wrap" justify="center" gap="xs">
<Flex direction="row">
<RingProgress
roundCaps
size={isTiny ? 32 : 48}
thickness={isTiny ? 2 : 4}
label={
<Center>
<IconCpu size={isTiny ? 12 : 20} />
</Center>
}
sections={[{ value: cpu, color: cpu > 75 ? "orange" : "green" }]}
/>
<Stack align="center" justify="center" gap={0}>
<Text fw={500} size={isTiny ? "xs" : "sm"}>
{t("widget.healthMonitoring.cluster.summary.cpu")}
</Text>
<Text size={isTiny ? "8px" : "xs"}>{cpu.toFixed(1)}%</Text>
</Stack>
</Flex>
<Flex>
<RingProgress
roundCaps
size={isTiny ? 32 : 48}
thickness={isTiny ? 2 : 4}
label={
<Center>
<IconBrain size={isTiny ? 12 : 20} />
</Center>
}
sections={[{ value: memory, color: memory > 75 ? "orange" : "green" }]}
/>
<Stack align="center" justify="center" gap={0}>
<Text size={isTiny ? "xs" : "sm"} fw={500}>
{t("widget.healthMonitoring.cluster.summary.memory")}
</Text>
<Text size={isTiny ? "8px" : "xs"}>{memory.toFixed(1)}%</Text>
</Stack>
</Flex>
{!cpu.hidden && (
<Flex direction="row">
<RingProgress
roundCaps
size={isTiny ? 32 : 48}
thickness={isTiny ? 2 : 4}
label={
<Center>
<IconCpu size={isTiny ? 12 : 20} />
</Center>
}
sections={[{ value: cpu.value, color: cpu.value > 75 ? "orange" : "green" }]}
/>
<Stack align="center" justify="center" gap={0}>
<Text fw={500} size={isTiny ? "xs" : "sm"}>
{t("widget.healthMonitoring.cluster.summary.cpu")}
</Text>
<Text size={isTiny ? "8px" : "xs"}>{cpu.value.toFixed(1)}%</Text>
</Stack>
</Flex>
)}
{!memory.hidden && (
<Flex>
<RingProgress
roundCaps
size={isTiny ? 32 : 48}
thickness={isTiny ? 2 : 4}
label={
<Center>
<IconBrain size={isTiny ? 12 : 20} />
</Center>
}
sections={[{ value: memory.value, color: memory.value > 75 ? "orange" : "green" }]}
/>
<Stack align="center" justify="center" gap={0}>
<Text size={isTiny ? "xs" : "sm"} fw={500}>
{t("widget.healthMonitoring.cluster.summary.memory")}
</Text>
<Text size={isTiny ? "8px" : "xs"}>{memory.value.toFixed(1)}%</Text>
</Stack>
</Flex>
)}
</Group>
</Center>
);
+85 -27
View File
@@ -8,34 +8,92 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("healthMonitoring", {
icon: IconHeartRateMonitor,
createOptions() {
return optionsBuilder.from((factory) => ({
fahrenheit: factory.switch({
defaultValue: false,
return optionsBuilder.from(
(factory) => ({
fahrenheit: factory.switch({
defaultValue: false,
}),
cpu: factory.switch({
defaultValue: true,
}),
memory: factory.switch({
defaultValue: true,
}),
showUptime: factory.switch({
defaultValue: true,
}),
fileSystem: factory.switch({
defaultValue: true,
}),
visibleClusterSections: factory.multiSelect({
options: [
{
value: "node",
label: (t) => t("widget.healthMonitoring.cluster.resource.node.name"),
},
{
value: "qemu",
label: (t) => t("widget.healthMonitoring.cluster.resource.qemu.name"),
},
{
value: "lxc",
label: (t) => t("widget.healthMonitoring.cluster.resource.lxc.name"),
},
{
value: "storage",
label: (t) => t("widget.healthMonitoring.cluster.resource.storage.name"),
},
] as const,
defaultValue: ["node", "qemu", "lxc", "storage"] as const,
}),
defaultTab: factory.select({
defaultValue: "system",
options: [
{ value: "system", label: "System" },
{ value: "cluster", label: "Cluster" },
] as const,
}),
sectionIndicatorRequirement: factory.select({
defaultValue: "all",
options: [
{ value: "all", label: "All active" },
{ value: "any", label: "Any active" },
] as const,
}),
}),
cpu: factory.switch({
defaultValue: true,
}),
memory: factory.switch({
defaultValue: true,
}),
fileSystem: factory.switch({
defaultValue: true,
}),
defaultTab: factory.select({
defaultValue: "system",
options: [
{ value: "system", label: "System" },
{ value: "cluster", label: "Cluster" },
] as const,
}),
sectionIndicatorRequirement: factory.select({
defaultValue: "all",
options: [
{ value: "all", label: "All active" },
{ value: "any", label: "Any active" },
] as const,
}),
}));
{
fahrenheit: {
shouldHide(_, integrationKinds) {
// File system is only shown on system health tab
return integrationKinds.every((kind) => kind === "proxmox") || integrationKinds.length === 0;
},
},
fileSystem: {
shouldHide(_, integrationKinds) {
// File system is only shown on system health tab
return integrationKinds.every((kind) => kind === "proxmox") || integrationKinds.length === 0;
},
},
showUptime: {
shouldHide(_, integrationKinds) {
// Uptime is only shown on cluster health tab
return !integrationKinds.includes("proxmox");
},
},
sectionIndicatorRequirement: {
shouldHide(_, integrationKinds) {
// Section indicator requirement is only shown on cluster health tab
return !integrationKinds.includes("proxmox");
},
},
visibleClusterSections: {
shouldHide(_, integrationKinds) {
// Cluster sections are only shown on cluster health tab
return !integrationKinds.includes("proxmox");
},
},
},
);
},
supportedIntegrations: getIntegrationKindsByCategory("healthMonitoring"),
errors: {
@@ -96,6 +96,7 @@ export default function MediaServerWidget({
gap="xs"
p="sm"
align="center"
justify="space-between"
style={{ pointerEvents: isEditMode ? "none" : undefined }}
>
<Stack gap={4} w="100%">
@@ -123,7 +124,7 @@ export default function MediaServerWidget({
))}
</Grid>
</Stack>
<Stack gap={4} w="100%" h="100%">
<Stack gap={4} w="100%">
<Text className="mediaRequests-stats-users-title" fw="bold" ta="center" size={isTiny ? "xs" : "sm"}>
{t("titles.users.main")} ({t("titles.users.requests")})
</Text>
+2 -2
View File
@@ -43,7 +43,7 @@ interface SelectInput<TOptions extends readonly SelectOption[]>
searchable?: boolean;
}
interface NumberInput extends CommonInput<number | ""> {
interface NumberInput extends CommonInput<number> {
validate: z.ZodNumber;
step?: number;
}
@@ -87,7 +87,7 @@ const optionsFactory = {
}),
number: (input: NumberInput) => ({
type: "number" as const,
defaultValue: input.defaultValue ?? ("" as const),
defaultValue: input.defaultValue ?? 0,
step: input.step,
withDescription: input.withDescription ?? false,
validate: input.validate,
+1 -1
View File
@@ -55,7 +55,7 @@ export default function RssFeed({ options }: WidgetComponentProps<"rssFeed">) {
dir={languageDir}
c="dimmed"
size="sm"
lineClamp={options.textLinesClamp as number}
lineClamp={options.textLinesClamp}
dangerouslySetInnerHTML={{ __html: feedEntry.description }}
/>
)}
Loaded 100 of 102 files, more files were not shown because too many files have changed in this diff. Show more