Compare commits

...
Author SHA1 Message Date
Meier Lukas 0513a8e6f5 feat(integrations): add suggestions to manage home page 2025-06-12 22:36:37 +02:00
homarr-renovate[bot] 0b200dc9e2 fix(deps): update dependency zod to ^3.25.62 (#3382)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-11 19:19:05 +00:00
homarr-renovate[bot] 377f4302c0 chore(deps): update dependency @types/react to v19.1.8 (#3381)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-11 14:20:18 +00:00
homarr-crowdin[bot] 740e9a5082 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-06-11 12:05:38 +00:00
homarr-renovate[bot] e7652ea4eb fix(deps): update dependency tsx to v4.20.1 (#3380)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-11 09:23:20 +00:00
homarr-renovate[bot] 863f935ef4 fix(deps): update dependency zod to ^3.25.61 (#3379)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-11 08:27:30 +00:00
homarr-renovate[bot] 7388c907a5 fix(deps): update dependency zod to ^3.25.60 (#3378)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-11 07:22:06 +00:00
homarr-renovate[bot] 3d07306e7d fix(deps): update dependency tsx to v4.20.0 (#3375)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-11 03:21:10 +00:00
homarr-renovate[bot] f37fc0edfe fix(deps): update dependency zod to ^3.25.58 (#3373)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-10 23:21:00 +00:00
Meier Lukas c59cdeda47 fix: csp prevents external videos (#3372) 2025-06-10 22:14:43 +02:00
homarr-renovate[bot] ad444e8fa7 fix(deps): update testcontainers-node monorepo to ^11.0.3 (#3369)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-10 18:41:04 +00:00
homarr-renovate[bot] fa52b55828 fix(deps): update dependency swagger-ui-react to ^5.24.1 (#3368)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-10 06:28:24 +00:00
homarr-renovate[bot] 1ce1113a8c chore(deps): update dependency @vitejs/plugin-react to ^4.5.2 (#3366)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-10 04:30:15 +00:00
homarr-renovate[bot] e7a9797484 fix(deps): update dependency zod to ^3.25.57 (#3367)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-10 03:23:52 +00:00
homarr-renovate[bot] d104aea8b8 chore(deps): update dependency @types/node to ^22.15.31 (#3365)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-10 03:22:22 +00:00
homarr-renovate[bot] ab3ee3bac1 fix(deps): update dependency @t3-oss/env-nextjs to ^0.13.8 (#3364)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-10 01:18:38 +00:00
homarr-crowdin[bot] 838066ac75 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-06-10 00:52:45 +00:00
homarr-renovate[bot] b1a559d801 fix(deps): update dependency sass to ^1.89.2 (#3362)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-09 20:24:11 +00:00
homarr-renovate[bot] eecf50d461 chore(deps): update dependency @types/react to v19.1.7 (#3361)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-09 20:23:08 +00:00
homarr-renovate[bot] d862ae03e3 fix(deps): update mantine monorepo to ^8.1.0 (#3360)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-09 18:28:49 +00:00
homarr-renovate[bot] f6da091577 fix(deps): update dependency typescript-eslint to ^8.34.0 (#3359)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-09 18:28:06 +00:00
homarr-renovate[bot] 22f0cd885e fix(deps): update dependency @t3-oss/env-nextjs to ^0.13.7 (#3357)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-09 16:25:59 +00:00
homarr-renovate[bot] 08c6ce9071 chore(deps): update vitest monorepo to ^3.2.3 (#3356)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-09 12:36:15 +00:00
homarr-renovate[bot] c3e1a53d09 chore(deps): update pnpm to v10.12.1 (#3354)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-08 15:20:30 +00:00
homarr-renovate[bot] 9e86de6e17 fix(deps): update dependency fast-xml-parser to ^5.2.5 (#3353)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-08 08:25:10 +00:00
homarr-renovate[bot] 98c4cd1e39 fix(deps): update testcontainers-node monorepo to ^11.0.2 (#3352)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-07 20:21:39 +00:00
Manuel f67702e1d3 feat: reduce cron job intervals (#3235) 2025-06-07 17:32:39 +00:00
Meier Lukas a9e68e13d5 fix(pi-hole): cached session not invalidated when changing password (#3351) 2025-06-07 16:20:21 +02:00
Meier Lukas 1b3770aafb fix: module not found sshcrypto.node during development (#3349) 2025-06-07 16:19:09 +02:00
37 changed files with 1585 additions and 1193 deletions

No files matched your search

+5 -19
View File
@@ -17,31 +17,16 @@ const withNextIntl = createNextIntlPlugin({
requestConfig: "../../packages/translation/src/request.ts",
});
interface WebpackConfig {
module: {
rules: {
test: RegExp;
loader: string;
}[];
};
}
const nextConfig: NextConfig = {
output: "standalone",
reactStrictMode: true,
/** We already do linting and typechecking as separate tasks in CI */
eslint: { ignoreDuringBuilds: true },
typescript: { ignoreBuildErrors: true },
webpack: (config: WebpackConfig, { isServer }) => {
if (isServer) {
config.module.rules.push({
test: /\.node$/,
loader: "node-loader",
});
}
return config;
},
/**
* dockerode is required in the external server packages because of https://github.com/homarr-labs/homarr/issues/612
*/
serverExternalPackages: ["dockerode"],
experimental: {
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
},
@@ -68,6 +53,7 @@ const nextConfig: NextConfig = {
form-action 'self';
img-src * data:;
font-src * data:;
media-src * data:;
`
.replace(/\s{2,}/g, " ")
.trim(),
+11 -11
View File
@@ -48,12 +48,12 @@
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0",
"@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",
"@mantine/colors-generator": "^8.1.0",
"@mantine/core": "^8.1.0",
"@mantine/dropzone": "^8.1.0",
"@mantine/hooks": "^8.1.0",
"@mantine/modals": "^8.1.0",
"@mantine/tiptap": "^8.1.0",
"@million/lint": "1.0.14",
"@tabler/icons-react": "^3.34.0",
"@tanstack/react-query": "^5.80.6",
@@ -81,20 +81,20 @@
"react-dom": "19.1.0",
"react-error-boundary": "^6.0.0",
"react-simple-code-editor": "^0.14.1",
"sass": "^1.89.1",
"sass": "^1.89.2",
"superjson": "2.2.2",
"swagger-ui-react": "^5.24.0",
"swagger-ui-react": "^5.24.1",
"use-deep-compare-effect": "^1.8.1",
"zod": "^3.25.56"
"zod": "^3.25.62"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "3.1.1",
"@types/node": "^22.15.30",
"@types/node": "^22.15.31",
"@types/prismjs": "^1.26.5",
"@types/react": "19.1.6",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.1.2",
+37 -1
View File
@@ -1,13 +1,16 @@
import Link from "next/link";
import { Card, Group, SimpleGrid, Space, Stack, Text } from "@mantine/core";
import { Card, Group, SimpleGrid, Space, Stack, Text, Title } from "@mantine/core";
import { IconArrowRight } from "@tabler/icons-react";
import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { isProviderEnabled } from "@homarr/auth/server";
import { getIntegrationName } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server";
import { IntegrationAvatar } from "@homarr/ui";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { catchTrpcNotFound } from "~/errors/trpc-catch-error";
import { createMetaTitle } from "~/metadata";
import { HeroBanner } from "./_components/hero-banner";
@@ -30,6 +33,8 @@ export async function generateMetadata() {
export default async function ManagementPage() {
const statistics = await api.home.getStats();
const session = await auth();
const isAdmin = session?.user.permissions.includes("admin") ?? false;
const suggestions = isAdmin ? await api.integration.suggestedIntegrations().catch(catchTrpcNotFound) : [];
const t = await getScopedI18n("management.page.home");
const links: LinkProps[] = [
@@ -102,6 +107,37 @@ export default async function ManagementPage() {
),
)}
</SimpleGrid>
{suggestions.length >= 1 && (
<>
<Space h="md" />
<Stack gap="sm">
<Stack gap={0}>
<Title order={3}>Suggested integrations</Title>
<Text c="dimmed">Based on your Docker containers</Text>
</Stack>
<SimpleGrid cols={{ xs: 1, sm: 2, md: 3 }}>
{suggestions.slice(0, 6).map((suggestion) => (
<Card key={suggestion.kind} component={Link} href="#" radius="lg">
<Group justify="space-between">
<Group>
<IntegrationAvatar kind={suggestion.kind} size="sm" />
<Stack gap={0}>
<Text size="md" fw={500}>
{getIntegrationName(suggestion.kind)}
</Text>
<Text c="dimmed" size="sm">
Show active media streams
</Text>
</Stack>
</Group>
<IconArrowRight />
</Group>
</Card>
))}
</SimpleGrid>
</Stack>
</>
)}
</>
);
}
+2 -2
View File
@@ -44,12 +44,12 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^22.15.30",
"@types/node": "^22.15.31",
"dotenv-cli": "^8.0.0",
"esbuild": "^0.25.5",
"eslint": "^9.28.0",
"prettier": "^3.5.3",
"tsx": "4.19.4",
"tsx": "4.20.1",
"typescript": "^5.8.3"
}
}
+1 -1
View File
@@ -26,7 +26,7 @@
"@homarr/redis": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"dotenv": "^16.5.0",
"tsx": "4.19.4",
"tsx": "4.20.1",
"ws": "^8.18.2"
},
"devDependencies": {
+6 -6
View File
@@ -39,21 +39,21 @@
"@semantic-release/npm": "^12.0.1",
"@semantic-release/release-notes-generator": "^14.0.3",
"@turbo/gen": "^2.5.4",
"@vitejs/plugin-react": "^4.5.1",
"@vitest/coverage-v8": "^3.2.2",
"@vitest/ui": "^3.2.2",
"@vitejs/plugin-react": "^4.5.2",
"@vitest/coverage-v8": "^3.2.3",
"@vitest/ui": "^3.2.3",
"conventional-changelog-conventionalcommits": "^9.0.0",
"cross-env": "^7.0.3",
"jsdom": "^26.1.0",
"prettier": "^3.5.3",
"semantic-release": "^24.2.5",
"testcontainers": "^11.0.1",
"testcontainers": "^11.0.3",
"turbo": "^2.5.4",
"typescript": "^5.8.3",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.2"
"vitest": "^3.2.3"
},
"packageManager": "pnpm@10.11.1",
"packageManager": "pnpm@10.12.1",
"engines": {
"node": ">=22.16.0"
},
+1 -1
View File
@@ -52,7 +52,7 @@
"react-dom": "19.1.0",
"superjson": "2.2.2",
"trpc-to-openapi": "^2.3.1",
"zod": "^3.25.56"
"zod": "^3.25.62"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
@@ -14,15 +14,19 @@ import {
integrationUserPermissions,
searchEngines,
} from "@homarr/db/schema";
import type { IntegrationSecretKind } from "@homarr/definitions";
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
import {
getIconUrl,
getIntegrationKindsByCategory,
getIntegrationName,
getPermissionsWithParents,
integrationDefs,
integrationKinds,
integrationSecretKindObject,
} from "@homarr/definitions";
import type { ContainerInfo, Port } from "@homarr/docker";
import { DockerSingleton } from "@homarr/docker";
import { env } from "@homarr/docker/env";
import { createIntegrationAsync } from "@homarr/integrations";
import { logger } from "@homarr/log";
import { byIdSchema } from "@homarr/validation/common";
@@ -527,8 +531,169 @@ export const integrationRouter = createTRPCRouter({
const integrationInstance = await createIntegrationAsync(ctx.integration);
return await integrationInstance.searchAsync(encodeURI(input.query));
}),
suggestedIntegrations: permissionRequiredProcedure.requiresPermission("admin").query(async ({ ctx }) => {
if (!env.ENABLE_DOCKER)
throw new TRPCError({
code: "NOT_FOUND",
message: "Docker is not enabled",
});
const dockerInstances = DockerSingleton.getInstances();
if (dockerInstances.length === 0) {
throw new TRPCError({
code: "NOT_FOUND",
message: "No Docker instances found",
});
}
// TODO: remove those that were already added
const containers = await Promise.all(
dockerInstances.map(({ instance, host, isSocket }) =>
instance.listContainers().then((containers) =>
containers.map((containerInfo) => ({
info: containerInfo,
host,
isSocket,
})),
),
),
).then((results) => results.flat());
// Default docker hostname consist out of first 12 characters of the container id
const homarrContainer = containers.find((container) => container.info.Id.slice(0, 12) === process.env.HOSTNAME);
const suggestions = containers
.map((container) => getSuggestionFromContainer(container, homarrContainer))
.filter((suggestion) => suggestion !== null);
const groupedSuggestions = suggestions.reduce(
(acc, suggestion) => {
acc[suggestion.kind] ??= [];
// is defined above
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
acc[suggestion.kind]!.push(suggestion);
return acc;
},
{} as Partial<Record<IntegrationKind, IntegrationSuggestion[]>>,
);
return Object.entries(groupedSuggestions)
.map(([kind, suggestions]) => ({
kind: kind as IntegrationKind,
suggestions,
}))
.sort((itemA, itemB) => {
if (itemA.suggestions.length !== itemB.suggestions.length)
return itemB.suggestions.length - itemA.suggestions.length;
const createdAtA = Math.max(...itemA.suggestions.map((suggestion) => suggestion.createdAt?.getTime() ?? 0));
const createdAtB = Math.max(...itemB.suggestions.map((suggestion) => suggestion.createdAt?.getTime() ?? 0));
return createdAtB - createdAtA;
});
}),
});
interface IntegrationSuggestion {
containerId: string;
name: string;
internalUrl: string | null;
externalUrl: string | null;
kind: IntegrationKind;
createdAt?: Date;
}
interface ContainerInfoWithHost {
info: ContainerInfo;
host: string;
isSocket: boolean | undefined;
}
const getSuggestionFromContainer = (
container: ContainerInfoWithHost,
homarrContainer: ContainerInfoWithHost | undefined,
): IntegrationSuggestion | null => {
const integrationKind = getSuggestionKind(container.info);
if (!integrationKind) return null;
return {
containerId: container.info.Id,
name: getIntegrationName(integrationKind),
kind: integrationKind,
internalUrl: getInternalUrl(container, homarrContainer),
externalUrl: getExternalUrl(container),
createdAt: container.info.Created ? new Date(container.info.Created * 1000) : undefined,
};
};
const getSuggestionKind = (container: ContainerInfo): IntegrationKind | null => {
const [image, _tag] = container.Image.split(":");
if (!image) return null;
return (
objectEntries(integrationDefs).find(
([, integration]) =>
"containerImages" in integration &&
integration.containerImages.some((containerImage) => containerImage.toLowerCase() === image.toLowerCase()),
)?.[0] ?? null
);
};
const calculatePortScore = (port: Port): number => {
let score = 0;
if (port.PublicPort) score += 10; // Public ports are more likely to be used
if (port.PrivatePort % 1000 === 80) score += 5; // Default http ports are most likely suffixed with 80 like 8080 or 80
if (port.PrivatePort % 1000 === 443) score += 10; // Default https ports are most likely suffixed with 443 like 8443 or 443
return score;
};
const getPort = (container: ContainerInfoWithHost): Port | null => {
const ports = container.info.Ports.filter((port) => port.Type === "tcp");
return (
ports
.map((port) => ({ port, score: calculatePortScore(port) }))
.sort((portA, portB) => portB.score - portA.score)
.at(0)?.port ?? null
);
};
const getInternalUrl = (
container: ContainerInfoWithHost,
homarrContainer: ContainerInfoWithHost | undefined,
): string | null => {
const port = getPort(container);
if (!port) return null;
const protocol = port.PrivatePort % 1000 === 443 ? "https" : "http";
if (homarrContainer) {
const matchingNetwork = Object.entries(homarrContainer.info.NetworkSettings.Networks).find(
([_key, homarrNetwork]) =>
Object.entries(container.info.NetworkSettings.Networks).some(
([_name, network]) => homarrNetwork.NetworkID === network.NetworkID,
),
)?.[1];
if (matchingNetwork) return `${protocol}://${matchingNetwork.IPAddress}:${port.PrivatePort}`;
}
// TODO: Let user specify the internal ip-address for integrations --> use as default before falling back to host.docker.internal
if (container.host === homarrContainer?.host && container.isSocket === homarrContainer.isSocket) {
return `${protocol}://host.docker.internal:${port.PublicPort}`;
}
// TODO: Change this
return `${protocol}://localhost:${port.PublicPort}`;
};
const getExternalUrl = (container: ContainerInfoWithHost): string | null => {
const port = getPort(container);
if (!port) return null;
const protocol = port.PrivatePort % 1000 === 443 ? "https" : "http";
// TODO: Use some labels like "traefik.host" to determine the external url
// For now we just use localhost as external url
return `${protocol}://localhost:${port.PublicPort}`;
};
interface UpdateSecretInput {
integrationId: string;
value: string;
+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.56"
"zod": "^3.25.62"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+1 -1
View File
@@ -34,7 +34,7 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"undici": "7.10.0",
"zod": "^3.25.56",
"zod": "^3.25.62",
"zod-validation-error": "^3.4.1"
},
"devDependencies": {
@@ -1,10 +1,10 @@
import { EVERY_5_SECONDS } from "@homarr/cron-jobs-core/expressions";
import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions";
import { dnsHoleRequestHandler } from "@homarr/request-handler/dns-hole";
import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler";
import { createCronJob } from "../../lib";
export const dnsHoleJob = createCronJob("dnsHole", EVERY_5_SECONDS).withCallback(
export const dnsHoleJob = createCronJob("dnsHole", EVERY_MINUTE).withCallback(
createRequestIntegrationJobHandler(dnsHoleRequestHandler.handler, {
widgetKinds: ["dnsHoleSummary", "dnsHoleControls"],
getInput: {
+2 -2
View File
@@ -1,6 +1,6 @@
import SuperJSON from "superjson";
import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions";
import { EVERY_10_MINUTES } from "@homarr/cron-jobs-core/expressions";
import { db, eq } from "@homarr/db";
import { items } from "@homarr/db/schema";
import { logger } from "@homarr/log";
@@ -10,7 +10,7 @@ import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds";
import type { WidgetComponentProps } from "../../../widgets";
import { createCronJob } from "../lib";
export const rssFeedsJob = createCronJob("rssFeeds", EVERY_5_MINUTES).withCallback(async () => {
export const rssFeedsJob = createCronJob("rssFeeds", EVERY_10_MINUTES).withCallback(async () => {
const rssItems = await db.query.items.findMany({
where: eq(items.kind, "rssFeed"),
});
+3 -3
View File
@@ -44,9 +44,9 @@
"@homarr/env": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@mantine/core": "^8.0.2",
"@mantine/core": "^8.1.0",
"@paralleldrive/cuid2": "^2.2.2",
"@testcontainers/mysql": "^11.0.1",
"@testcontainers/mysql": "^11.0.3",
"better-sqlite3": "^11.10.0",
"dotenv": "^16.5.0",
"drizzle-kit": "^0.31.1",
@@ -63,7 +63,7 @@
"esbuild": "^0.25.5",
"eslint": "^9.28.0",
"prettier": "^3.5.3",
"tsx": "4.19.4",
"tsx": "4.20.1",
"typescript": "^5.8.3"
}
}
+3 -3
View File
@@ -24,15 +24,15 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"fast-xml-parser": "^5.2.4",
"zod": "^3.25.56"
"fast-xml-parser": "^5.2.5",
"zod": "^3.25.62"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0",
"tsx": "4.19.4",
"tsx": "4.20.1",
"typescript": "^5.8.3"
}
}
+94
View File
@@ -16,6 +16,7 @@ interface integrationDefinition {
iconUrl: string;
secretKinds: AtLeastOneOf<IntegrationSecretKind[]>; // at least one secret kind set is required
category: AtLeastOneOf<IntegrationCategory>;
containerImages?: string[]; // Optional, used for container images to suggest in the UI
}
export const integrationDefs = {
@@ -24,30 +25,45 @@ export const integrationDefs = {
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sabnzbd.svg",
category: ["downloadClient", "usenet"],
containerImages: ["linuxserver/sabnzbd", "lscr.io/linuxserver/sabnzbd", "ghcr.io/linuxserver/sabnzbd"],
},
nzbGet: {
name: "NZBGet",
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/nzbget.svg",
category: ["downloadClient", "usenet"],
containerImages: [
"nzbgetcom/nzbget",
"ghcr.io/nzbgetcom/nzbget",
"linuxserver/nzbget",
"lscr.io/linuxserver/nzbget",
"ghcr.io/linuxserver/nzbget",
],
},
deluge: {
name: "Deluge",
secretKinds: [["password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/deluge.svg",
category: ["downloadClient", "torrent"],
containerImages: ["linuxserver/deluge", "lscr.io/linuxserver/deluge", "ghcr.io/linuxserver/deluge"],
},
transmission: {
name: "Transmission",
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/transmission.svg",
category: ["downloadClient", "torrent"],
containerImages: [
"linuxserver/transmission",
"lscr.io/linuxserver/transmission",
"ghcr.io/linuxserver/transmission",
],
},
qBittorrent: {
name: "qBittorrent",
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/qbittorrent.svg",
category: ["downloadClient", "torrent"],
containerImages: ["linuxserver/qbittorrent", "lscr.io/linuxserver/qbittorrent", "ghcr.io/linuxserver/qbittorrent"],
},
aria2: {
name: "Aria2",
@@ -60,78 +76,144 @@ export const integrationDefs = {
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sonarr.svg",
category: ["calendar"],
containerImages: [
"hotio/sonarr",
"ghcr.io/hotio/sonarr",
"linuxserver/sonarr",
"lscr.io/linuxserver/sonarr",
"ghcr.io/linuxserver/sonarr",
],
},
radarr: {
name: "Radarr",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/radarr.svg",
category: ["calendar"],
containerImages: [
"hotio/radarr",
"ghcr.io/hotio/radarr",
"linuxserver/radarr",
"lscr.io/linuxserver/radarr",
"ghcr.io/linuxserver/radarr",
],
},
lidarr: {
name: "Lidarr",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/lidarr.svg",
category: ["calendar"],
containerImages: [
"hotio/lidarr",
"ghcr.io/hotio/lidarr",
"linuxserver/lidarr",
"lscr.io/linuxserver/lidarr",
"ghcr.io/linuxserver/lidarr",
],
},
readarr: {
name: "Readarr",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/readarr.svg",
category: ["calendar"],
containerImages: [
"hotio/readarr",
"ghcr.io/hotio/readarr",
"linuxserver/readarr",
"lscr.io/linuxserver/readarr",
"ghcr.io/linuxserver/readarr",
],
},
prowlarr: {
name: "Prowlarr",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/prowlarr.svg",
category: ["indexerManager"],
containerImages: [
"hotio/prowlarr",
"ghcr.io/hotio/prowlarr",
"linuxserver/prowlarr",
"lscr.io/linuxserver/prowlarr",
"ghcr.io/linuxserver/prowlarr",
],
},
jellyfin: {
name: "Jellyfin",
secretKinds: [["username", "password"], ["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyfin.svg",
category: ["mediaService"],
containerImages: [
"jellyfin/jellyfin",
"ghcr.io/jellyfin/jellyfin",
"linuxserver/jellyfin",
"lscr.io/linuxserver/jellyfin",
"ghcr.io/linuxserver/jellyfin",
],
},
emby: {
name: "Emby",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/emby.svg",
category: ["mediaService"],
containerImages: [
"emby/embyserver",
"emby/embyserver_arm64v8",
"emby/embyserver_arm32v7",
"linuxserver/emby",
"lscr.io/linuxserver/emby",
"ghcr.io/linuxserver/emby",
],
},
plex: {
name: "Plex",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/plex.svg",
category: ["mediaService"],
containerImages: ["plexinc/pms-docker", "linuxserver/plex", "lscr.io/linuxserver/plex", "ghcr.io/linuxserver/plex"],
},
jellyseerr: {
name: "Jellyseerr",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyseerr.svg",
category: ["mediaSearch", "mediaRequest", "search"],
containerImages: ["fallenbagel/jellyseerr", "ghcr.io/fallenbagel/jellyseerr"],
},
overseerr: {
name: "Overseerr",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/overseerr.svg",
category: ["mediaSearch", "mediaRequest", "search"],
containerImages: [
"sct/overseerr",
"linuxserver/overseerr",
"lscr.io/linuxserver/overseerr",
"ghcr.io/linuxserver/overseerr",
],
},
piHole: {
name: "Pi-hole",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/pi-hole.svg",
category: ["dnsHole"],
containerImages: ["pihole/pihole"],
},
adGuardHome: {
name: "AdGuard Home",
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/adguard-home.svg",
category: ["dnsHole"],
containerImages: ["adguard/adguardhome"],
},
homeAssistant: {
name: "Home Assistant",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/home-assistant.svg",
category: ["smartHomeServer"],
containerImages: [
"homeassistant/home-assistant",
"linuxserver/home-assistant",
"lscr.io/linuxserver/home-assistant",
"ghcr.io/linuxserver/home-assistant",
],
},
openmediavault: {
name: "OpenMediaVault",
@@ -144,12 +226,14 @@ export const integrationDefs = {
secretKinds: [[]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/dashdot.png",
category: ["healthMonitoring"],
containerImages: ["mauricenino/dashdot", "ghcr.io/mauricenino/dashdot"],
},
tdarr: {
name: "Tdarr",
secretKinds: [[], ["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/tdarr.png",
category: ["mediaTranscoding"],
containerImages: ["haveagitgat/tdarr", "ghcr.io/haveagitgat/tdarr"],
},
proxmox: {
name: "Proxmox",
@@ -168,6 +252,16 @@ export const integrationDefs = {
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/unifi.png",
category: ["networkController"],
containerImages: [
"jacobalberty/unifi",
"ghcr.io/jacobalberty/unifi",
"linuxserver/unifi-controller",
"lscr.io/linuxserver/unifi-controller",
"ghcr.io/linuxserver/unifi-controller",
"linuxserver/unifi-network-application",
"lscr.io/linuxserver/unifi-network-application",
"ghcr.io/linuxserver/unifi-network-application",
],
},
} as const satisfies Record<string, integrationDefinition>;
+2 -1
View File
@@ -5,6 +5,7 @@ import { env } from "./env";
export interface DockerInstance {
host: string;
instance: Docker;
isSocket?: boolean;
}
export class DockerSingleton {
@@ -14,7 +15,7 @@ export class DockerSingleton {
const hostVariable = env.DOCKER_HOSTNAMES;
const portVariable = env.DOCKER_PORTS;
if (hostVariable === undefined || portVariable === undefined) {
return [{ host: "socket", instance: new Docker() }];
return [{ host: "socket", instance: new Docker(), isSocket: true }];
}
const hostnames = hostVariable.split(",");
const ports = portVariable.split(",");
+2 -2
View File
@@ -23,8 +23,8 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@t3-oss/env-nextjs": "^0.13.6",
"zod": "^3.25.56"
"@t3-oss/env-nextjs": "^0.13.8",
"zod": "^3.25.62"
},
"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.2",
"zod": "^3.25.56"
"@mantine/form": "^8.1.0",
"zod": "^3.25.62"
},
"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.2",
"@mantine/core": "^8.1.0",
"react": "19.1.0",
"zod": "^3.25.56"
"zod": "^3.25.62"
},
"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.56"
"zod": "^3.25.62"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
@@ -77,6 +77,7 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
protected async testingAsync({ fetchAsync }: IntegrationTestingInput): Promise<TestingResult> {
const sessionId = await this.getSessionAsync(fetchAsync);
await this.clearSessionAsync(sessionId, fetchAsync);
await this.sessionStore.clearAsync();
return { success: true };
}
+1 -1
View File
@@ -27,7 +27,7 @@
"ioredis": "5.6.1",
"superjson": "2.2.2",
"winston": "3.17.0",
"zod": "^3.25.56"
"zod": "^3.25.62"
},
"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.2",
"@mantine/core": "^8.1.0",
"@tabler/icons-react": "^3.34.0",
"dayjs": "^1.11.13",
"next": "15.3.3",
"react": "19.1.0",
"react-dom": "19.1.0",
"zod": "^3.25.56"
"zod": "^3.25.62"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+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.2",
"@mantine/hooks": "^8.0.2",
"@mantine/core": "^8.1.0",
"@mantine/hooks": "^8.1.0",
"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.2",
"@mantine/notifications": "^8.1.0",
"@tabler/icons-react": "^3.34.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.2",
"@mantine/hooks": "^8.0.2",
"@mantine/core": "^8.1.0",
"@mantine/hooks": "^8.1.0",
"adm-zip": "0.5.16",
"next": "15.3.3",
"react": "19.1.0",
"react-dom": "19.1.0",
"superjson": "2.2.2",
"zod": "^3.25.56",
"zod": "^3.25.62",
"zod-form-data": "^2.0.7"
},
"devDependencies": {
+1 -1
View File
@@ -23,7 +23,7 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"zod": "^3.25.56"
"zod": "^3.25.62"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+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.2",
"@mantine/dates": "^8.1.0",
"next": "15.3.3",
"react": "19.1.0",
"react-dom": "19.1.0"
+3 -3
View File
@@ -33,9 +33,9 @@
"@homarr/settings": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^8.0.2",
"@mantine/hooks": "^8.0.2",
"@mantine/spotlight": "^8.0.2",
"@mantine/core": "^8.1.0",
"@mantine/hooks": "^8.1.0",
"@mantine/spotlight": "^8.1.0",
"@tabler/icons-react": "^3.34.0",
"jotai": "^2.12.5",
"next": "15.3.3",
+144 -144
View File
@@ -263,14 +263,14 @@
}
},
"removeImage": {
"label": "删除图像",
"label": "删除头像",
"confirm": "确定要移除这张图片吗?",
"notification": {
"success": {
"message": "图像删除成功"
},
"error": {
"message": "无法删除图像"
"message": "无法删除头像"
}
}
}
@@ -722,8 +722,8 @@
},
"statusCode": {
"title": "响应错误",
"description": "",
"otherDescription": "",
"description": "收到来自<url></url>的意外{statusCode} ({reason})响应。请验证URL是否指向集成的基本URL。",
"otherDescription": "从<url></url>收到意外的{statusCode}响应。请验证URL是否指向集成的基本URL。",
"reason": {
"badRequest": "错误请求",
"notFound": "未找到",
@@ -752,7 +752,7 @@
},
"extract": {
"title": "CA证书提取失败",
"message": ""
"message": "只有自签署证书链可以自动获取。如果使用的是自签名证书,请确保手动上传CA证书。您可以找到有关如何执行此操作的说明<docsLink></docsLink>。"
}
},
"action": {
@@ -760,70 +760,70 @@
"label": "重试创建"
},
"trust": {
"label": ""
"label": "信任证书"
},
"upload": {
"label": ""
"label": "上传证书"
}
},
"hostnameMismatch": {
"confirm": {
"title": "",
"message": ""
"title": "信任主机名不匹配",
"message": "您确定要信任主机名不匹配的证书吗?"
},
"notification": {
"success": {
"title": "",
"message": ""
"title": "可信证书",
"message": "将主机名添加到受信任证书列表中"
},
"error": {
"title": "",
"message": ""
"title": "信任证书失败",
"message": "主机名不匹配的证书不可信"
}
}
},
"selfSigned": {
"confirm": {
"title": "",
"message": ""
"title": "信任自签名证书",
"message": "您确定要信任此自签名证书吗?"
},
"notification": {
"success": {
"title": "",
"message": ""
"title": "可信证书",
"message": "已添加证书到可信证书列表"
},
"error": {
"title": "",
"message": ""
"title": "信任证书失败",
"message": "向受信任证书列表添加证书失败"
}
}
},
"details": {
"title": "",
"description": "",
"title": "详情",
"description": "在决定信任证书信息之前,请检查该证书信息。",
"content": {
"action": "",
"title": ""
"action": "显示内容",
"title": "PEM 证书"
}
}
},
"request": {
"title": "",
"title": "错误请求",
"description": {
"connection": {
"hostUnreachable": "",
"networkUnreachable": "",
"refused": "",
"reset": ""
"hostUnreachable": "无法连接到服务器。这通常意味着主机离线或无法从您的网络访问。",
"networkUnreachable": "网络不可达。请检查网络连接或网络配置。",
"refused": "服务器拒绝连接。它可能没有运行或正在拒绝指定端口上的请求。",
"reset": "服务器意外关闭了连接。如果服务器不稳定或重新启动,就会发生这种情况。"
},
"dns": {
"notFound": "",
"timeout": "",
"noAnswer": ""
"notFound": "找不到服务器地址。请检查URL是否有拼写错误或无效的域名。",
"timeout": "DNS查找超时。这可能是暂时的问题,请稍后再试。",
"noAnswer": "DNS服务器没有返回有效的响应。域可能存在,但没有有效记录。"
},
"timeout": {
"aborted": "",
"timeout": ""
"aborted": "请求在完成之前被中止。这可能是由于用户操作或系统超时造成的。",
"timeout": "请求耗时太长。请检查您的网络或稍后再试。"
}
}
}
@@ -895,8 +895,8 @@
"message": "该路径可能不正确"
},
"tooManyRequests": {
"title": "",
"message": ""
"title": "指定时间内请求过多",
"message": "请求太多了。您可能会受到速率限制或被目标系统拒绝"
}
}
},
@@ -1004,7 +1004,7 @@
"cancel": "取消",
"delete": "删除",
"discard": "Discard",
"close": "",
"close": "关闭",
"confirm": "确认",
"continue": "继续",
"previous": "上一步",
@@ -1220,7 +1220,7 @@
"label": "集成"
},
"title": {
"label": ""
"label": "标题"
},
"customCssClasses": {
"label": "自定义 CSS 类"
@@ -1758,7 +1758,7 @@
"label": "显示内存信息"
},
"showUptime": {
"label": ""
"label": "显示运行时间"
},
"fileSystem": {
"label": "显示文件系统信息"
@@ -1767,7 +1767,7 @@
"label": "默认标签"
},
"visibleClusterSections": {
"label": ""
"label": "可见集群部分"
},
"sectionIndicatorRequirement": {
"label": "部分指标要求"
@@ -1839,11 +1839,11 @@
}
},
"dockerContainers": {
"name": "",
"description": "",
"name": "Docker 状态",
"description": "您的容器状态 (这个小部件只能用管理员权限添加)",
"option": {},
"error": {
"internalServerError": ""
"internalServerError": "获取容器统计信息失败"
}
},
"common": {
@@ -1912,8 +1912,8 @@
"description": "显示媒体服务器上的当前流",
"option": {
"showOnlyPlaying": {
"label": "",
"description": ""
"label": "只显示当前正在播放",
"description": "禁用此功能对plex不起作用"
}
},
"items": {
@@ -1946,7 +1946,7 @@
"label": "显示标记为已完成的 Torrent 条目"
},
"showCompletedHttp": {
"label": ""
"label": "显示标记为已完成的杂项"
},
"activeTorrentThreshold": {
"label": "隐藏在此阈值下已完成的 Torrent (kiB/s)"
@@ -1961,8 +1961,8 @@
"label": "使用过滤器来计算比率"
},
"limitPerIntegration": {
"label": "",
"description": ""
"label": "限制每个集成的项目",
"description": "这将限制每个集成显示的项目数量,而不是全局项目"
}
},
"errors": {
@@ -2095,7 +2095,7 @@
"approved": "已批准",
"declined": "已拒绝",
"failed": "失败",
"completed": ""
"completed": "已完成"
},
"toBeDetermined": "待定"
},
@@ -2199,106 +2199,106 @@
}
},
"releases": {
"name": "",
"description": "",
"name": "发布",
"description": "显示特定版本正则表达式中当前版本的列表。",
"option": {
"newReleaseWithin": {
"label": "",
"description": ""
"label": "内部新版本",
"description": "使用示例:1w(1周),10M(10个月)。支持单位类型h(小时),d(天),w(周),M(月),y(年)。留空不突出显示新版本。"
},
"staleReleaseWithin": {
"label": "",
"description": ""
"label": "内部过期发布",
"description": "使用示例:1w(1周),10M(10个月)。支持单位类型h(小时),d(天),w(周),M(月),y(年)。留空不突出显示过时的版本。"
},
"showOnlyHighlighted": {
"label": "",
"description": ""
"label": "只显示高亮显示",
"description": "只显示新的或过时的版本。如上所述。"
},
"showDetails": {
"label": ""
"label": "展开详情"
},
"topReleases": {
"label": "",
"description": ""
"label": "热门发布",
"description": "要显示的最新版本的最大数量。零表示没有限制。"
},
"repositories": {
"label": "",
"label": "仓库",
"addRepository": {
"label": ""
"label": "添加仓库"
},
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
"label": "从 Docker 导入",
"loading": "正在加载 docker 映像",
"noImagesFound": "没有找到 docker 映像",
"listFoundImages": "找到的映像列表",
"listAlreadyImportedImages": "已导入的映像列表",
"allImagesAlreadyImported": "所有映像已导入",
"onlyAdminCanImport": "只有管理员才能从 docker 导入"
},
"provider": {
"label": ""
"label": "提供者"
},
"identifier": {
"label": "",
"placeholder": ""
"label": "标识符",
"placeholder": "姓名或所有者名称"
},
"name": {
"label": ""
"label": "名称"
},
"versionFilter": {
"label": "",
"label": "版本过滤器",
"prefix": {
"label": ""
"label": "前缀"
},
"precision": {
"label": "",
"label": "精度",
"options": {
"none": ""
"none": "空"
}
},
"suffix": {
"label": ""
"label": "后缀"
},
"regex": {
"label": ""
"label": "正则表达式"
}
},
"edit": {
"label": ""
"label": "编辑"
},
"editForm": {
"title": "",
"title": "编辑仓库",
"cancel": {
"label": ""
"label": "取消"
},
"confirm": {
"label": ""
"label": "确认"
}
},
"importForm": {
"title": ""
"title": "从 Docker 导入"
},
"example": {
"label": ""
"label": "示例"
},
"invalid": ""
"invalid": "仓库定义无效,请检查值"
}
},
"not-found": "",
"pre-release": "",
"archived": "",
"forked": "",
"starsCount": "",
"forksCount": "",
"issuesCount": "",
"openProjectPage": "",
"openReleasePage": "",
"releaseDescription": "",
"created": "",
"not-found": "未找到",
"pre-release": "预发行",
"archived": "已归档",
"forked": "复刻成功",
"starsCount": "星",
"forksCount": "复刻",
"issuesCount": "打开问题",
"openProjectPage": "打开项目页面",
"openReleasePage": "打开发布页面",
"releaseDescription": "发布说明",
"created": "已创建",
"error": {
"label": "",
"label": "错误",
"options": {
"noMatchingVersion": ""
"noMatchingVersion": "没有找到匹配的版本"
}
}
},
@@ -2306,56 +2306,56 @@
"option": {},
"card": {
"vpn": {
"countConnected": ""
"countConnected": "{count} 已连接"
}
},
"error": {
"integrationsDisconnected": "",
"unknownContentOption": ""
"integrationsDisconnected": "没有可用的数据,所有集成已断开",
"unknownContentOption": "网络控制器摘要小部件的未知内容选项: "
},
"name": "",
"description": ""
"name": "网络控制器概述",
"description": "显示网络控制器摘要(如Unifi Controller)"
},
"networkControllerStatus": {
"card": {
"variants": {
"wired": {
"name": ""
"name": "有线"
},
"wifi": {
"name": ""
"name": "Wi-Fi"
}
},
"users": {
"label": ""
"label": "用户数"
},
"guests": {
"label": ""
"label": "游客"
}
},
"option": {
"content": {
"option": {
"wifi": {
"label": ""
"label": "Wi-Fi"
},
"wired": {
"label": ""
"label": "有线"
}
},
"label": ""
"label": "小部件内容"
}
},
"error": {
"integrationsDisconnected": "",
"unknownContentOption": ""
"integrationsDisconnected": "没有可用的数据,所有集成已断开",
"unknownContentOption": "网络状态小部件的未知内容选项: "
},
"name": "",
"description": ""
"name": "网络状态",
"description": "在网络上显示已连接的设备"
},
"networkController": {
"error": {
"internalServerError": ""
"internalServerError": "获取网络控制器概述失败"
}
}
},
@@ -2478,10 +2478,10 @@
},
"backgroundImageUrl": {
"label": "背景图片 URL",
"placeholder": "",
"placeholder": "开始输入以搜索本地映像",
"group": {
"your": "你的图像",
"other": ""
"your": "你的映像",
"other": "其他映像"
}
},
"backgroundImageAttachment": {
@@ -3123,7 +3123,7 @@
"label": "网络控制器"
},
"dockerContainers": {
"label": ""
"label": "Docker 容器"
}
}
},
@@ -3189,7 +3189,7 @@
"updated": "已更新 {when}",
"search": "搜索 {count} 容器",
"selected": "{totalCount} 容器的 {selectCount}",
"footer": ""
"footer": "总计 {count} 容器"
},
"field": {
"name": {
@@ -3209,10 +3209,10 @@
},
"stats": {
"cpu": {
"label": ""
"label": "CPU"
},
"memory": {
"label": ""
"label": "内存"
}
},
"containerImage": {
@@ -3223,7 +3223,7 @@
}
},
"action": {
"title": "",
"title": "操作",
"start": {
"label": "开始",
"notification": {
@@ -3668,7 +3668,7 @@
"certificates": {
"label": "证书",
"hostnames": {
"label": ""
"label": "主机名"
}
}
},
@@ -4069,25 +4069,25 @@
"certificate": {
"field": {
"hostname": {
"label": ""
"label": "主机名"
},
"subject": {
"label": ""
"label": "主题"
},
"issuer": {
"label": ""
"label": "发行者"
},
"validFrom": {
"label": ""
"label": "有效期自"
},
"validTo": {
"label": ""
"label": "有效期至"
},
"serialNumber": {
"label": ""
"label": "序号"
},
"fingerprint": {
"label": ""
"label": "指纹"
}
},
"page": {
@@ -4098,19 +4098,19 @@
"title": "还没有证书"
},
"invalid": {
"title": "",
"description": ""
"title": "无效证书",
"description": "解析证书失败"
},
"expires": "到期时间 {when}",
"toHostnames": ""
"toHostnames": "信任的主机名"
},
"hostnames": {
"title": "",
"description": "",
"title": "信任的证书主机名",
"description": "某些证书不允许指定域名使用 Homarr 请求他们, 由于这个原因,所有信任的主机名及其证书缩略图都被用来绕过这些限制。",
"noResults": {
"title": ""
"title": "还没有主机名"
},
"toCertificates": ""
"toCertificates": "证书"
}
},
"action": {
@@ -4142,16 +4142,16 @@
}
},
"removeHostname": {
"label": "",
"confirm": "",
"label": "删除受信任的主机名",
"confirm": "您确定要删除此受信任的主机名吗?这可能会导致一些集成停止工作。",
"notification": {
"success": {
"title": "",
"message": ""
"title": "主机名已删除",
"message": "主机名已成功删除"
},
"error": {
"title": "",
"message": ""
"title": "主机名未删除",
"message": "无法删除主机名"
}
}
}
File diff suppressed because it is too large. Load diff
+8 -8
View File
@@ -2227,13 +2227,13 @@
"label": "Depo Ekle"
},
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
"label": "Docker'dan içe aktar",
"loading": "Docker imajları yükleniyor",
"noImagesFound": "Hiç docker imajı bulunamadı",
"listFoundImages": "Bulunan imajların listesi",
"listAlreadyImportedImages": "Daha önce içe aktarılan imajların listesi",
"allImagesAlreadyImported": "Tüm imajlar daha önce içe aktarıldı",
"onlyAdminCanImport": "Sadece yöneticiler docker'dan içe aktarabilir"
},
"provider": {
"label": "Sağlayıcı"
@@ -2276,7 +2276,7 @@
}
},
"importForm": {
"title": ""
"title": "Docker'dan içe aktar"
},
"example": {
"label": "Örnek"
+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.2",
"@mantine/dates": "^8.0.2",
"@mantine/hooks": "^8.0.2",
"@mantine/core": "^8.1.0",
"@mantine/dates": "^8.1.0",
"@mantine/hooks": "^8.1.0",
"@tabler/icons-react": "^3.34.0",
"mantine-react-table": "2.0.0-beta.9",
"next": "15.3.3",
+1 -1
View File
@@ -24,7 +24,7 @@
"dependencies": {
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"zod": "^3.25.56",
"zod": "^3.25.62",
"zod-form-data": "^2.0.7"
},
"devDependencies": {
+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.2",
"@mantine/core": "^8.0.2",
"@mantine/hooks": "^8.0.2",
"@mantine/charts": "^8.1.0",
"@mantine/core": "^8.1.0",
"@mantine/hooks": "^8.1.0",
"@tabler/icons-react": "^3.34.0",
"@tiptap/extension-color": "2.14.0",
"@tiptap/extension-highlight": "2.14.0",
@@ -76,7 +76,7 @@
"react-markdown": "^10.1.0",
"recharts": "^2.15.3",
"video.js": "^8.23.3",
"zod": "^3.25.56"
"zod": "^3.25.62"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+659 -550
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -24,7 +24,7 @@
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"typescript-eslint": "^8.33.1"
"typescript-eslint": "^8.34.0"
},
"devDependencies": {
"@homarr/prettier-config": "workspace:^0.1.0",