chore(release): automatic release v1.37.0

This commit is contained in:
homarr-releases[bot]
2025-09-12 19:13:23 +00:00
committed by GitHub
119 changed files with 2301 additions and 1176 deletions

View File

@@ -33,6 +33,7 @@ body:
options:
# The below comment is used to insert a new version with on-release.yml
#NEXT_VERSION#
- 1.36.1
- 1.36.0
- 1.35.1
- 1.35.0

View File

@@ -57,7 +57,7 @@ jobs:
if: env.SKIP_RELEASE == 'false'
with:
persist-credentials: false
- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
if: env.SKIP_RELEASE == 'false'
with:
node-version: 22.19.0

View File

@@ -32,7 +32,7 @@ jobs:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: ${{ matrix.node-version }}

View File

@@ -42,9 +42,11 @@ const nextConfig: NextConfig = {
headers: [
{
key: "Content-Security-Policy",
// worker-src / media-src with blob: is necessary for video.js, see https://github.com/homarr-labs/homarr/issues/3912 and https://stackoverflow.com/questions/65792855/problem-with-video-js-and-content-security-policy-csp
value: `
default-src 'self';
script-src * 'unsafe-inline' 'unsafe-eval';
worker-src * blob:;
base-uri 'self';
connect-src *;
style-src * 'unsafe-inline';
@@ -53,7 +55,7 @@ const nextConfig: NextConfig = {
form-action 'self';
img-src * data:;
font-src * data:;
media-src * data:;
media-src * data: blob:;
`
.replace(/\s{2,}/g, " ")
.trim(),

View File

@@ -61,17 +61,17 @@
"@tanstack/react-query": "^5.87.1",
"@tanstack/react-query-devtools": "^5.87.1",
"@tanstack/react-query-next-experimental": "^5.87.1",
"@trpc/client": "^11.5.0",
"@trpc/next": "^11.5.0",
"@trpc/react-query": "^11.5.0",
"@trpc/server": "^11.5.0",
"@trpc/client": "^11.5.1",
"@trpc/next": "^11.5.1",
"@trpc/react-query": "^11.5.1",
"@trpc/server": "^11.5.1",
"@xterm/addon-canvas": "^0.7.0",
"@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "^5.5.0",
"chroma-js": "^3.1.2",
"clsx": "^2.1.1",
"dayjs": "^1.11.18",
"dotenv": "^17.2.1",
"dotenv": "^17.2.2",
"flag-icons": "^7.5.0",
"glob": "^11.0.3",
"jotai": "^2.13.1",
@@ -83,9 +83,9 @@
"react-dom": "19.1.1",
"react-error-boundary": "^6.0.0",
"react-simple-code-editor": "^0.14.1",
"sass": "^1.91.0",
"sass": "^1.92.1",
"superjson": "2.2.2",
"swagger-ui-react": "^5.28.0",
"swagger-ui-react": "^5.28.1",
"use-deep-compare-effect": "^1.8.1",
"zod": "^4.1.5"
},
@@ -94,13 +94,13 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "3.1.1",
"@types/node": "^22.18.0",
"@types/node": "^22.18.1",
"@types/prismjs": "^1.26.5",
"@types/react": "19.1.12",
"@types/react-dom": "19.1.9",
"@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.2.1",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"node-loader": "^2.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.2"

View File

@@ -61,6 +61,10 @@ const getBoardAndPermissionsAsync = async (params: Awaited<Props["params"]>) =>
notFound();
}
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
notFound();
}
throw error;
}
};

View File

@@ -45,6 +45,10 @@ export const createBoardLayout = <TParams extends Params>({
notFound();
}
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
notFound();
}
throw error;
});
const colorScheme = await getCurrentColorSchemeAsync();

View File

@@ -1,9 +1,11 @@
import {
IconCode,
IconGrid3x3,
IconKey,
IconMessage,
IconPassword,
IconPasswordUser,
IconPlug,
IconServer,
IconUser,
} from "@tabler/icons-react";
@@ -21,4 +23,7 @@ export const integrationSecretIcons = {
topic: IconMessage,
opnsenseApiKey: IconKey,
opnsenseApiSecret: IconPassword,
githubAppId: IconCode,
githubInstallationId: IconPlug,
privateKey: IconKey,
} satisfies Record<IntegrationSecretKind, TablerIcon>;

View File

@@ -1,7 +1,7 @@
"use client";
import type { ChangeEventHandler, FocusEventHandler } from "react";
import { PasswordInput, TextInput } from "@mantine/core";
import { PasswordInput, Textarea, TextInput } from "@mantine/core";
import { integrationSecretKindObject } from "@homarr/definitions";
import type { IntegrationSecretKind } from "@homarr/definitions";
@@ -14,9 +14,9 @@ interface IntegrationSecretInputProps {
label?: string;
kind: IntegrationSecretKind;
value?: string;
onChange: ChangeEventHandler<HTMLInputElement>;
onFocus?: FocusEventHandler<HTMLInputElement>;
onBlur?: FocusEventHandler<HTMLInputElement>;
onChange: ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>;
onFocus?: FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
onBlur?: FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
error?: string;
}
@@ -31,6 +31,19 @@ export const IntegrationSecretInput = (props: IntegrationSecretInputProps) => {
const PublicSecretInput = ({ kind, ...props }: IntegrationSecretInputProps) => {
const t = useI18n();
const Icon = integrationSecretIcons[kind];
const { multiline } = integrationSecretKindObject[kind];
if (multiline) {
return (
<Textarea
{...props}
label={props.label ?? t(`integration.secrets.kind.${kind}.label`)}
w="100%"
leftSection={<Icon size={20} stroke={1.5} />}
autosize
minRows={2}
/>
);
}
return (
<TextInput
@@ -45,6 +58,21 @@ const PublicSecretInput = ({ kind, ...props }: IntegrationSecretInputProps) => {
const PrivateSecretInput = ({ kind, ...props }: IntegrationSecretInputProps) => {
const t = useI18n();
const Icon = integrationSecretIcons[kind];
const { multiline } = integrationSecretKindObject[kind];
if (multiline) {
return (
<Textarea
{...props}
label={props.label ?? t(`integration.secrets.kind.${kind}.label`)}
description={t("integration.secrets.secureNotice")}
w="100%"
leftSection={<Icon size={20} stroke={1.5} />}
autosize
minRows={2}
/>
);
}
return (
<PasswordInput

View File

@@ -21,6 +21,7 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/analytics": "workspace:^0.1.0",
"@homarr/auth": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/cron-job-api": "workspace:^0.1.0",
"@homarr/cron-jobs": "workspace:^0.1.0",
@@ -32,12 +33,13 @@
"@homarr/log": "workspace:^",
"@homarr/ping": "workspace:^0.1.0",
"@homarr/redis": "workspace:^0.1.0",
"@homarr/request-handler": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0",
"dayjs": "^1.11.18",
"dotenv": "^17.2.1",
"fastify": "^5.5.0",
"dotenv": "^17.2.2",
"fastify": "^5.6.0",
"superjson": "2.2.2",
"undici": "7.15.0"
},
@@ -45,10 +47,10 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^22.18.0",
"@types/node": "^22.18.1",
"dotenv-cli": "^10.0.0",
"esbuild": "^0.25.9",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"prettier": "^3.6.2",
"tsx": "4.20.4",
"typescript": "^5.9.2"

View File

@@ -26,7 +26,6 @@ export class JobManager implements IJobManager {
logger.info(`Updating cron job interval name="${name}" expression="${cron}"`);
const job = this.jobGroup.getJobRegistry().get(name);
if (!job) throw new Error(`Job ${name} not found`);
if (job.cronExpression === "never") throw new Error(`Job ${name} cannot be updated as it is set to "never"`);
if (!validateCron(cron)) {
throw new Error(`Invalid cron expression: ${cron}`);
}
@@ -45,7 +44,6 @@ export class JobManager implements IJobManager {
logger.info(`Disabling cron job name="${name}"`);
const job = this.jobGroup.getJobRegistry().get(name);
if (!job) throw new Error(`Job ${name} not found`);
if (job.cronExpression === "never") throw new Error(`Job ${name} cannot be disabled as it is set to "never"`);
await this.updateConfigurationAsync(name, { isEnabled: false });
await this.jobGroup.stopAsync(name);

View File

@@ -13,6 +13,7 @@ import { db } from "@homarr/db";
import { logger } from "@homarr/log";
import { JobManager } from "./job-manager";
import { onStartAsync } from "./on-start";
const server = fastify({
maxParamLength: 5000,
@@ -32,6 +33,7 @@ server.register(fastifyTRPCPlugin, {
});
void (async () => {
await onStartAsync();
await jobGroup.initializeAsync();
await jobGroup.startAllAsync();

View File

@@ -0,0 +1,7 @@
import { invalidateUpdateCheckerCacheAsync } from "./invalidate-update-checker-cache";
import { cleanupSessionsAsync } from "./session-cleanup";
export async function onStartAsync() {
await cleanupSessionsAsync();
await invalidateUpdateCheckerCacheAsync();
}

View File

@@ -0,0 +1,18 @@
import { logger } from "@homarr/log";
import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
const localLogger = logger.child({ module: "invalidateUpdateCheckerCache" });
/**
* Invalidates the update checker cache on startup to ensure fresh data.
* It is important as we want to avoid showing pending updates after the update to latest version.
*/
export async function invalidateUpdateCheckerCacheAsync() {
try {
const handler = updateCheckerRequestHandler.handler({});
await handler.invalidateAsync();
localLogger.debug("Update checker cache invalidated");
} catch (error) {
localLogger.error(new Error("Failed to invalidate update checker cache", { cause: error }));
}
}

View File

@@ -0,0 +1,39 @@
import { env } from "@homarr/auth/env";
import { db, eq, inArray } from "@homarr/db";
import { sessions, users } from "@homarr/db/schema";
import { supportedAuthProviders } from "@homarr/definitions";
import { logger } from "@homarr/log";
const localLogger = logger.child({ module: "sessionCleanup" });
/**
* Deletes sessions for users that have inactive auth providers.
* Sessions from other providers are deleted so they can no longer be used.
*/
export async function cleanupSessionsAsync() {
try {
const currentAuthProviders = env.AUTH_PROVIDERS;
const inactiveAuthProviders = supportedAuthProviders.filter((provider) => !currentAuthProviders.includes(provider));
const subQuery = db
.select({ id: users.id })
.from(users)
.where(inArray(users.provider, inactiveAuthProviders))
.as("sq");
const sessionsWithInactiveProviders = await db
.select({ userId: sessions.userId })
.from(sessions)
.rightJoin(subQuery, eq(sessions.userId, subQuery.id));
const userIds = sessionsWithInactiveProviders.map(({ userId }) => userId).filter((value) => value !== null);
await db.delete(sessions).where(inArray(sessions.userId, userIds));
if (sessionsWithInactiveProviders.length > 0) {
localLogger.info(`Deleted sessions for inactive providers count=${userIds.length}`);
} else {
localLogger.debug("No sessions to delete");
}
} catch (error) {
localLogger.error(new Error("Failed to clean up sessions", { cause: error }));
}
}

View File

@@ -25,7 +25,7 @@
"@homarr/log": "workspace:^",
"@homarr/redis": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"dotenv": "^17.2.1",
"dotenv": "^17.2.2",
"tsx": "4.20.4",
"ws": "^8.18.3"
},
@@ -35,7 +35,7 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/ws": "^8.18.1",
"esbuild": "^0.25.9",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"prettier": "^3.6.2",
"typescript": "^5.9.2"
}

View File

@@ -293,6 +293,13 @@
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/truenas" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/truenas.svg" alt="TrueNAS" width="90" height="90" />
<br/>
<p align="center">TrueNAS</p>
</a>
</td></tr>
<tr><td align="center">
<a href="https://homarr.dev/docs/integrations/unifi-controller" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/unifi.png" alt="Unifi Controller" width="90" height="90" />
<br/>

View File

@@ -41,7 +41,7 @@
"@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^11.0.5",
"@semantic-release/npm": "^12.0.2",
"@semantic-release/release-notes-generator": "^14.0.3",
"@semantic-release/release-notes-generator": "^14.1.0",
"@testcontainers/redis": "^11.5.1",
"@turbo/gen": "^2.5.6",
"@vitejs/plugin-react": "^5.0.2",
@@ -51,14 +51,14 @@
"cross-env": "^10.0.0",
"jsdom": "^26.1.0",
"prettier": "^3.6.2",
"semantic-release": "^24.2.7",
"semantic-release": "^24.2.8",
"testcontainers": "^11.5.1",
"turbo": "^2.5.6",
"typescript": "^5.9.2",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4"
},
"packageManager": "pnpm@10.15.0",
"packageManager": "pnpm@10.15.1",
"engines": {
"node": ">=22.19.0"
},
@@ -75,9 +75,9 @@
"tree-sitter-json"
],
"overrides": {
"@babel/helpers@<7.26.10": ">=7.28.3",
"@babel/runtime@<7.26.10": ">=7.28.3",
"axios@>=1.0.0 <1.8.2": ">=1.11.0",
"@babel/helpers@<7.26.10": ">=7.28.4",
"@babel/runtime@<7.26.10": ">=7.28.4",
"axios@>=1.0.0 <1.8.2": ">=1.12.1",
"brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1",
"brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1",
"esbuild@<=0.24.2": ">=0.25.9",
@@ -87,15 +87,16 @@
"nanoid@>=4.0.0 <5.0.9": ">=5.1.5",
"prismjs@<1.30.0": ">=1.30.0",
"proxmox-api>undici": "7.15.0",
"rollup@>=4.0.0 <4.22.4": ">=4.50.0",
"rollup@>=4.0.0 <4.22.4": ">=4.50.1",
"sha.js@<=2.4.11": ">=2.4.12",
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.0",
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.0",
"tmp@<=0.2.3": ">=0.2.5",
"vite@>=5.0.0 <=5.4.18": ">=7.1.3"
"vite@>=5.0.0 <=5.4.18": ">=7.1.5"
},
"patchedDependencies": {
"@types/node-unifi": "patches/@types__node-unifi.patch"
"@types/node-unifi": "patches/@types__node-unifi.patch",
"trpc-to-openapi": "patches/trpc-to-openapi.patch"
},
"allowUnusedPatches": true,
"ignoredBuiltDependencies": [

View File

@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -43,10 +43,10 @@
"@homarr/validation": "workspace:^0.1.0",
"@kubernetes/client-node": "^1.3.0",
"@tanstack/react-query": "^5.87.1",
"@trpc/client": "^11.5.0",
"@trpc/react-query": "^11.5.0",
"@trpc/server": "^11.5.0",
"@trpc/tanstack-react-query": "^11.5.0",
"@trpc/client": "^11.5.1",
"@trpc/react-query": "^11.5.1",
"@trpc/server": "^11.5.1",
"@trpc/tanstack-react-query": "^11.5.1",
"lodash.clonedeep": "^4.5.0",
"next": "15.5.2",
"react": "19.1.1",
@@ -59,7 +59,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"prettier": "^3.6.2",
"typescript": "^5.9.2"
}

View File

@@ -25,16 +25,6 @@ export const testConnectionAsync = async (
integrationUrl: integration.url,
});
const formSecrets = integration.secrets
.filter((secret) => secret.value !== null)
.map((secret) => ({
...secret,
// We ensured above that the value is not null
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
value: secret.value!,
source: "form" as const,
}));
const decryptedDbSecrets = dbSecrets
.map((secret) => {
try {
@@ -55,6 +45,15 @@ export const testConnectionAsync = async (
})
.filter((secret) => secret !== null);
const formSecrets = integration.secrets
.map((secret) => ({
...secret,
// If the value is not defined in the form (because we only changed other values) we use the existing value from the db if it exists
value: secret.value ?? decryptedDbSecrets.find((dbSecret) => dbSecret.kind === secret.kind)?.value ?? null,
source: "form" as const,
}))
.filter((secret): secret is SourcedIntegrationSecret<"form"> => secret.value !== null);
const sourcedSecrets = [...formSecrets, ...decryptedDbSecrets];
const secretKinds = getSecretKindOption(integration.kind, sourcedSecrets);
@@ -89,10 +88,10 @@ export const testConnectionAsync = async (
return result;
};
interface SourcedIntegrationSecret {
interface SourcedIntegrationSecret<TSource extends string = "db" | "form"> {
kind: IntegrationSecretKind;
value: string;
source: "db" | "form";
source: TSource;
}
const getSecretKindOption = (kind: IntegrationKind, sourcedSecrets: SourcedIntegrationSecret[]) => {
@@ -111,7 +110,9 @@ const getSecretKindOption = (kind: IntegrationKind, sourcedSecrets: SourcedInteg
}
const onlyFormSecretsKindOptions = matchingSecretKindOptions.filter((secretKinds) =>
sourcedSecrets.filter((secret) => secretKinds.includes(secret.kind)).every((secret) => secret.source === "form"),
secretKinds.every((secretKind) =>
sourcedSecrets.find((secret) => secret.kind === secretKind && secret.source === "form"),
),
);
if (onlyFormSecretsKindOptions.length >= 1) {

View File

@@ -265,4 +265,77 @@ describe("testConnectionAsync should run test connection of integration", () =>
],
});
});
test("with input of existing github app", async () => {
// Arrange
const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync");
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
factorySpy.mockReturnValue(
Promise.resolve({
testConnectionAsync: async () => await Promise.resolve({ success: true }),
} as homarrIntegrations.PiHoleIntegrationV6),
);
optionsSpy.mockReturnValue([[], ["githubAppId", "githubInstallationId", "privateKey"]]);
const integration = {
id: "new",
name: "GitHub",
url: "https://api.github.com",
kind: "github" as const,
secrets: [
{
kind: "githubAppId" as const,
value: "345",
},
{
kind: "githubInstallationId" as const,
value: "456",
},
{
kind: "privateKey" as const,
value: null,
},
],
};
const dbSecrets = [
{
kind: "githubAppId" as const,
value: "123.encrypted" as const,
},
{
kind: "githubInstallationId" as const,
value: "234.encrypted" as const,
},
{
kind: "privateKey" as const,
value: "privateKey.encrypted" as const,
},
];
// Act
await testConnectionAsync(integration, dbSecrets);
// Assert
expect(factorySpy).toHaveBeenCalledWith({
id: "new",
name: "GitHub",
url: "https://api.github.com",
kind: "github" as const,
decryptedSecrets: [
expect.objectContaining({
kind: "githubAppId",
value: "345",
}),
expect.objectContaining({
kind: "githubInstallationId",
value: "456",
}),
expect.objectContaining({
kind: "privateKey",
value: "privateKey",
}),
],
});
});
});

View File

@@ -9,7 +9,7 @@ import { createTRPCRouter, publicProcedure } from "../../trpc";
export const healthMonitoringRouter = createTRPCRouter({
getSystemHealthStatus: publicProcedure
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "mock"))
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "truenas", "mock"))
.query(async ({ ctx }) => {
return await Promise.all(
ctx.integrations.map(async (integration) => {
@@ -26,7 +26,7 @@ export const healthMonitoringRouter = createTRPCRouter({
);
}),
subscribeSystemHealthStatus: publicProcedure
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "mock"))
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "truenas", "mock"))
.subscription(({ ctx }) => {
return observable<{ integrationId: string; healthInfo: SystemHealthMonitoring; timestamp: Date }>((emit) => {
const unsubscribes: (() => void)[] = [];

View File

@@ -47,7 +47,7 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/bcrypt": "6.0.0",
"@types/cookies": "0.9.1",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"prettier": "^3.6.2",
"typescript": "^5.9.2"
}

View File

@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -30,7 +30,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -28,14 +28,14 @@
"@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"dotenv": "^17.2.1"
"dotenv": "^17.2.2"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"esbuild": "^0.25.9",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -33,6 +33,7 @@
"dayjs": "^1.11.18",
"dns-caching": "^0.2.5",
"next": "15.5.2",
"octokit": "^5.0.3",
"react": "19.1.1",
"react-dom": "19.1.1",
"undici": "7.15.0",
@@ -43,7 +44,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -3,3 +3,4 @@ export * from "./fetch-http-error-handler";
export * from "./ofetch-http-error-handler";
export * from "./axios-http-error-handler";
export * from "./tsdav-http-error-handler";
export * from "./octokit-http-error-handler";

View File

@@ -0,0 +1,24 @@
import { RequestError as OctokitRequestError } from "octokit";
import type { AnyRequestError } from "../request-error";
import { ResponseError } from "../response-error";
import { HttpErrorHandler } from "./http-error-handler";
export class OctokitHttpErrorHandler extends HttpErrorHandler {
/**
* I wasn't able to get a request error triggered. Therefore we ignore them for now
* and just forward them as unknown errors
*/
handleRequestError(_: unknown): AnyRequestError | undefined {
return undefined;
}
handleResponseError(error: unknown): ResponseError | undefined {
if (!(error instanceof OctokitRequestError)) return undefined;
return new ResponseError({
status: error.status,
url: error.response?.url,
});
}
}

View File

@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -30,9 +30,9 @@
"@homarr/cron-jobs": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@tanstack/react-query": "^5.87.1",
"@trpc/client": "^11.5.0",
"@trpc/server": "^11.5.0",
"@trpc/tanstack-react-query": "^11.5.0",
"@trpc/client": "^11.5.1",
"@trpc/server": "^11.5.1",
"@trpc/tanstack-react-query": "^11.5.1",
"node-cron": "^4.2.1",
"react": "19.1.1",
"zod": "^4.1.5"
@@ -43,7 +43,7 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-cron": "^3.0.11",
"@types/react": "19.1.12",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -33,7 +33,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-cron": "^3.0.11",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -72,8 +72,6 @@ const createCallback = <TAllowedNames extends string, TName extends TAllowedName
where: (cronJobConfigurations, { eq }) => eq(cronJobConfigurations.name, name),
});
if (defaultCronExpression === "never") return null;
const scheduledTask = createTask(
configuration?.cronExpression ?? defaultCronExpression,
() => void catchingCallbackAsync(),
@@ -120,7 +118,7 @@ export const createCronJobCreator = <TAllowedNames extends string = string>(
options: CreateCronJobOptions = { runOnStart: false },
) => {
creatorOptions.logger.logDebug(`Validating cron expression '${defaultCronExpression}' for job: ${name}`);
if (defaultCronExpression !== "never" && !validate(defaultCronExpression)) {
if (!validate(defaultCronExpression)) {
throw new Error(`Invalid cron expression '${defaultCronExpression}' for job '${name}'`);
}
creatorOptions.logger.logDebug(`Cron job expression '${defaultCronExpression}' for job ${name} is valid`);
@@ -132,8 +130,6 @@ export const createCronJobCreator = <TAllowedNames extends string = string>(
// This is a type guard to check if the cron expression is valid and give the user a type hint
return returnValue as unknown as ValidateCron<TExpression> extends true
? typeof returnValue
: TExpression extends "never"
? typeof returnValue
: "Invalid cron expression";
: "Invalid cron expression";
};
};

View File

@@ -8,4 +8,3 @@ export const EVERY_10_MINUTES = checkCron("*/10 * * * *") satisfies string;
export const EVERY_HOUR = checkCron("0 * * * *") satisfies string;
export const EVERY_DAY = checkCron("0 0 * * */1") satisfies string;
export const EVERY_WEEK = checkCron("0 0 * * 1") satisfies string;
export const NEVER = "never";

View File

@@ -45,7 +45,6 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
}
const scheduledTask = await job.createTaskAsync();
if (!scheduledTask) continue;
tasks.set(job.name, scheduledTask);
}

View File

@@ -44,7 +44,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -21,7 +21,6 @@ import { refreshNotificationsJob } from "./jobs/integrations/notifications";
import { minecraftServerStatusJob } from "./jobs/minecraft-server-status";
import { pingJob } from "./jobs/ping";
import { rssFeedsJob } from "./jobs/rss-feeds";
import { sessionCleanupJob } from "./jobs/session-cleanup";
import { updateCheckerJob } from "./jobs/update-checker";
import { createCronJobGroup } from "./lib";
@@ -39,7 +38,6 @@ export const jobGroup = createCronJobGroup({
rssFeeds: rssFeedsJob,
indexerManager: indexerManagerJob,
healthMonitoring: healthMonitoringJob,
sessionCleanup: sessionCleanupJob,
updateChecker: updateCheckerJob,
mediaTranscoding: mediaTranscodingJob,
minecraftServerStatus: minecraftServerStatusJob,

View File

@@ -1,38 +0,0 @@
import { env } from "@homarr/auth/env";
import { NEVER } from "@homarr/cron-jobs-core/expressions";
import { db, eq, inArray } from "@homarr/db";
import { sessions, users } from "@homarr/db/schema";
import { supportedAuthProviders } from "@homarr/definitions";
import { logger } from "@homarr/log";
import { createCronJob } from "../lib";
/**
* Deletes sessions for users that have inactive auth providers.
* Sessions from other providers are deleted so they can no longer be used.
*/
export const sessionCleanupJob = createCronJob("sessionCleanup", NEVER, {
runOnStart: true,
}).withCallback(async () => {
const currentAuthProviders = env.AUTH_PROVIDERS;
const inactiveAuthProviders = supportedAuthProviders.filter((provider) => !currentAuthProviders.includes(provider));
const subQuery = db
.select({ id: users.id })
.from(users)
.where(inArray(users.provider, inactiveAuthProviders))
.as("sq");
const sessionsWithInactiveProviders = await db
.select({ userId: sessions.userId })
.from(sessions)
.rightJoin(subQuery, eq(sessions.userId, subQuery.id));
const userIds = sessionsWithInactiveProviders.map(({ userId }) => userId).filter((value) => value !== null);
await db.delete(sessions).where(inArray(sessions.userId, userIds));
if (sessionsWithInactiveProviders.length > 0) {
logger.info(`Deleted sessions for inactive providers count=${userIds.length}`);
} else {
logger.debug("No sessions to delete");
}
});

View File

@@ -54,11 +54,11 @@
"@testcontainers/mysql": "^11.5.1",
"@testcontainers/postgresql": "^11.5.1",
"better-sqlite3": "^12.2.0",
"dotenv": "^17.2.1",
"dotenv": "^17.2.2",
"drizzle-kit": "^0.31.4",
"drizzle-orm": "^0.44.5",
"drizzle-zod": "^0.8.3",
"mysql2": "3.14.3",
"mysql2": "3.14.4",
"pg": "^8.16.3",
"superjson": "2.2.2"
},
@@ -70,7 +70,7 @@
"@types/pg": "^8.15.5",
"dotenv-cli": "^10.0.0",
"esbuild": "^0.25.9",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"prettier": "^3.6.2",
"tsx": "4.20.4",
"typescript": "^5.9.2"

View File

@@ -31,7 +31,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"tsx": "4.20.4",
"typescript": "^5.9.2"
}

View File

@@ -4,16 +4,19 @@ import type { AtLeastOneOf } from "@homarr/common/types";
import { createDocumentationLink } from "./docs";
export const integrationSecretKindObject = {
apiKey: { isPublic: false },
username: { isPublic: true },
password: { isPublic: false },
tokenId: { isPublic: true },
realm: { isPublic: true },
personalAccessToken: { isPublic: false },
topic: { isPublic: true },
opnsenseApiKey: { isPublic: false },
opnsenseApiSecret: { isPublic: false },
} satisfies Record<string, { isPublic: boolean }>;
apiKey: { isPublic: false, multiline: false },
username: { isPublic: true, multiline: false },
password: { isPublic: false, multiline: false },
tokenId: { isPublic: true, multiline: false },
realm: { isPublic: true, multiline: false },
personalAccessToken: { isPublic: false, multiline: false },
topic: { isPublic: true, multiline: false },
opnsenseApiKey: { isPublic: false, multiline: false },
opnsenseApiSecret: { isPublic: false, multiline: false },
privateKey: { isPublic: false, multiline: true },
githubAppId: { isPublic: true, multiline: false },
githubInstallationId: { isPublic: true, multiline: false },
} satisfies Record<string, { isPublic: boolean; multiline: boolean }>;
export const integrationSecretKinds = objectKeys(integrationSecretKindObject);
@@ -211,7 +214,7 @@ export const integrationDefs = {
},
github: {
name: "Github",
secretKinds: [[], ["personalAccessToken"]],
secretKinds: [[], ["personalAccessToken"], ["githubAppId", "githubInstallationId", "privateKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/github.svg",
category: ["releasesProvider"],
defaultUrl: "https://api.github.com",
@@ -259,7 +262,7 @@ export const integrationDefs = {
},
gitHubContainerRegistry: {
name: "GitHub Container Registry",
secretKinds: [[], ["personalAccessToken"]],
secretKinds: [[], ["personalAccessToken"], ["githubAppId", "githubInstallationId", "privateKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/github.svg",
category: ["releasesProvider"],
defaultUrl: "https://api.github.com",
@@ -280,6 +283,13 @@ export const integrationDefs = {
category: ["notifications"],
documentationUrl: createDocumentationLink("/docs/integrations/ntfy"),
},
truenas: {
name: "TrueNAS",
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/truenas.svg",
category: ["healthMonitoring"],
documentationUrl: createDocumentationLink("/docs/integrations/truenas"),
},
// This integration only returns mock data, it is used during development (but can also be used in production by directly going to the create page)
mock: {
name: "Mock",

View File

@@ -33,7 +33,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/dockerode": "^3.3.43",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

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",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -37,7 +37,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -31,7 +31,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -33,7 +33,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/bcrypt": "6.0.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -25,10 +25,10 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@ctrl/deluge": "^7.1.1",
"@ctrl/qbittorrent": "^9.6.0",
"@ctrl/transmission": "^7.2.0",
"@gitbeaker/rest": "^43.4.0",
"@ctrl/deluge": "^7.2.0",
"@ctrl/qbittorrent": "^9.7.0",
"@ctrl/transmission": "^7.3.0",
"@gitbeaker/rest": "^43.5.0",
"@homarr/certificates": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
@@ -40,6 +40,7 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@jellyfin/sdk": "^0.11.0",
"@octokit/auth-app": "^8.1.0",
"maria2": "^0.4.1",
"node-ical": "^0.20.1",
"octokit": "^5.0.3",
@@ -55,7 +56,7 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-unifi": "^2.5.1",
"@types/xml2js": "^0.4.14",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -23,7 +23,6 @@ export const statusResponseSchema = z.object({
dns_addresses: z.array(z.string()),
dns_port: z.number().positive(),
http_port: z.number().positive(),
protection_disabled_duration: z.number(),
protection_enabled: z.boolean(),
dhcp_available: z.boolean(),
running: z.boolean(),

View File

@@ -38,6 +38,7 @@ import { PlexIntegration } from "../plex/plex-integration";
import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration";
import { ProxmoxIntegration } from "../proxmox/proxmox-integration";
import { QuayIntegration } from "../quay/quay-integration";
import { TrueNasIntegration } from "../truenas/truenas-integration";
import { UnifiControllerIntegration } from "../unifi-controller/unifi-controller-integration";
import type { Integration, IntegrationInput } from "./integration";
@@ -114,6 +115,7 @@ export const integrationCreators = {
quay: QuayIntegration,
ntfy: NTFYIntegration,
mock: MockIntegration,
truenas: TrueNasIntegration,
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
type IntegrationInstanceOfKind<TKind extends keyof typeof integrationCreators> = {

View File

@@ -1,6 +1,7 @@
import {
AxiosHttpErrorHandler,
FetchHttpErrorHandler,
OctokitHttpErrorHandler,
OFetchHttpErrorHandler,
TsdavHttpErrorHandler,
} from "@homarr/common/server";
@@ -11,3 +12,4 @@ export const integrationFetchHttpErrorHandler = new IntegrationHttpErrorHandler(
export const integrationOFetchHttpErrorHandler = new IntegrationHttpErrorHandler(new OFetchHttpErrorHandler());
export const integrationAxiosHttpErrorHandler = new IntegrationHttpErrorHandler(new AxiosHttpErrorHandler());
export const integrationTsdavHttpErrorHandler = new IntegrationHttpErrorHandler(new TsdavHttpErrorHandler());
export const integrationOctokitHttpErrorHandler = new IntegrationHttpErrorHandler(new OctokitHttpErrorHandler());

View File

@@ -1,11 +1,14 @@
import { createAppAuth } from "@octokit/auth-app";
import { Octokit, RequestError } from "octokit";
import type { fetch } from "undici";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { logger } from "@homarr/log";
import { HandleIntegrationErrors } from "../base/errors/decorator";
import { integrationOctokitHttpErrorHandler } from "../base/errors/http";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration";
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
@@ -18,23 +21,21 @@ import type {
const localLogger = logger.child({ module: "GitHubContainerRegistryIntegration" });
@HandleIntegrationErrors([integrationOctokitHttpErrorHandler])
export class GitHubContainerRegistryIntegration extends Integration implements ReleasesProviderIntegration {
private static readonly userAgent = "Homarr-Lab/Homarr:GitHubContainerRegistryIntegration";
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const headers: RequestInit["headers"] = {
"User-Agent": GitHubContainerRegistryIntegration.userAgent,
};
const api = this.getApi(input.fetchAsync);
if (this.hasSecretValue("personalAccessToken"))
headers.Authorization = `Bearer ${this.getSecretValue("personalAccessToken")}`;
const response = await input.fetchAsync(this.url("/octocat"), {
headers,
});
if (!response.ok) {
return TestConnectionError.StatusResult(response);
if (this.hasSecretValue("personalAccessToken")) {
await api.rest.users.getAuthenticated();
} else if (this.hasSecretValue("githubAppId")) {
await api.rest.apps.getInstallation({
installation_id: Number(this.getSecretValue("githubInstallationId")),
});
} else {
await api.request("GET /octocat");
}
return {
@@ -131,15 +132,38 @@ export class GitHubContainerRegistryIntegration extends Integration implements R
}
}
private getApi() {
private getAuthProperties(): Pick<OctokitOptions, "auth" | "authStrategy"> {
if (this.hasSecretValue("personalAccessToken"))
return {
auth: this.getSecretValue("personalAccessToken"),
};
if (this.hasSecretValue("githubAppId"))
return {
authStrategy: createAppAuth,
auth: {
appId: this.getSecretValue("githubAppId"),
installationId: this.getSecretValue("githubInstallationId"),
privateKey: this.getSecretValue("privateKey"),
} satisfies Parameters<typeof createAppAuth>[0],
};
return {};
}
private getApi(customFetch?: typeof fetch) {
return new Octokit({
baseUrl: this.url("/").origin,
request: {
fetch: fetchWithTrustedCertificatesAsync,
fetch: customFetch ?? fetchWithTrustedCertificatesAsync,
},
userAgent: GitHubContainerRegistryIntegration.userAgent,
throttle: { enabled: false }, // Disable throttling for this integration, Octokit will retry by default after a set time, thus delaying the repsonse to the user in case of errors. Errors will be shown to the user, no need to retry the request.
...(this.hasSecretValue("personalAccessToken") ? { auth: this.getSecretValue("personalAccessToken") } : {}),
// Disable throttling for this integration, Octokit will retry by default after a set time,
// thus delaying the repsonse to the user in case of errors. Errors will be shown to the user, no need to retry the request.
throttle: { enabled: false },
...this.getAuthProperties(),
});
}
}
type OctokitOptions = Exclude<ConstructorParameters<typeof Octokit>[0], undefined>;

View File

@@ -1,11 +1,14 @@
import { Octokit, RequestError } from "octokit";
import { createAppAuth } from "@octokit/auth-app";
import { Octokit, RequestError as OctokitRequestError } from "octokit";
import type { fetch } from "undici";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { logger } from "@homarr/log";
import { HandleIntegrationErrors } from "../base/errors/decorator";
import { integrationOctokitHttpErrorHandler } from "../base/errors/http";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration";
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
@@ -18,23 +21,21 @@ import type {
const localLogger = logger.child({ module: "GithubIntegration" });
@HandleIntegrationErrors([integrationOctokitHttpErrorHandler])
export class GithubIntegration extends Integration implements ReleasesProviderIntegration {
private static readonly userAgent = "Homarr-Lab/Homarr:GithubIntegration";
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const headers: RequestInit["headers"] = {
"User-Agent": GithubIntegration.userAgent,
};
const api = this.getApi(input.fetchAsync);
if (this.hasSecretValue("personalAccessToken"))
headers.Authorization = `Bearer ${this.getSecretValue("personalAccessToken")}`;
const response = await input.fetchAsync(this.url("/octocat"), {
headers,
});
if (!response.ok) {
return TestConnectionError.StatusResult(response);
if (this.hasSecretValue("personalAccessToken")) {
await api.rest.users.getAuthenticated();
} else if (this.hasSecretValue("githubAppId")) {
await api.rest.apps.getInstallation({
installation_id: Number(this.getSecretValue("githubInstallationId")),
});
} else {
await api.request("GET /octocat");
}
return {
@@ -91,7 +92,7 @@ export class GithubIntegration extends Integration implements ReleasesProviderIn
return getLatestRelease(releasesProviderResponse, repository, details);
} catch (error) {
const errorMessage = error instanceof RequestError ? error.message : String(error);
const errorMessage = error instanceof OctokitRequestError ? error.message : String(error);
localLogger.warn(`Failed to get releases for ${owner}\\${name} with Github integration`, {
owner,
@@ -131,21 +132,44 @@ export class GithubIntegration extends Integration implements ReleasesProviderIn
localLogger.warn(`Failed to get details for ${owner}\\${name} with Github integration`, {
owner,
name,
error: error instanceof RequestError ? error.message : String(error),
error: error instanceof OctokitRequestError ? error.message : String(error),
});
return undefined;
}
}
private getApi() {
private getAuthProperties(): Pick<OctokitOptions, "auth" | "authStrategy"> {
if (this.hasSecretValue("personalAccessToken"))
return {
auth: this.getSecretValue("personalAccessToken"),
};
if (this.hasSecretValue("githubAppId"))
return {
authStrategy: createAppAuth,
auth: {
appId: this.getSecretValue("githubAppId"),
installationId: this.getSecretValue("githubInstallationId"),
privateKey: this.getSecretValue("privateKey"),
} satisfies Parameters<typeof createAppAuth>[0],
};
return {};
}
private getApi(customFetch?: typeof fetch) {
return new Octokit({
baseUrl: this.url("/").origin,
request: {
fetch: fetchWithTrustedCertificatesAsync,
fetch: customFetch ?? fetchWithTrustedCertificatesAsync,
},
userAgent: GithubIntegration.userAgent,
throttle: { enabled: false }, // Disable throttling for this integration, Octokit will retry by default after a set time, thus delaying the repsonse to the user in case of errors. Errors will be shown to the user, no need to retry the request.
...(this.hasSecretValue("personalAccessToken") ? { auth: this.getSecretValue("personalAccessToken") } : {}),
// Disable throttling for this integration, Octokit will retry by default after a set time,
// thus delaying the repsonse to the user in case of errors. Errors will be shown to the user, no need to retry the request.
throttle: { enabled: false },
...this.getAuthProperties(),
});
}
}
type OctokitOptions = Exclude<ConstructorParameters<typeof Octokit>[0], undefined>;

View File

@@ -21,6 +21,7 @@ export { PiHoleIntegrationV5 } from "./pi-hole/v5/pi-hole-integration-v5";
export { PiHoleIntegrationV6 } from "./pi-hole/v6/pi-hole-integration-v6";
export { PlexIntegration } from "./plex/plex-integration";
export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration";
export { TrueNasIntegration } from "./truenas/truenas-integration";
export { OPNsenseIntegration } from "./opnsense/opnsense-integration";
// Types

View File

@@ -15,7 +15,7 @@ export interface SystemHealthMonitoring {
"1min": number;
"5min": number;
"15min": number;
};
} | null;
rebootRequired: boolean;
availablePkgUpdates: number;
cpuTemp: number | undefined;

View File

@@ -265,7 +265,9 @@ const mapType = (type: string): "movie" | "tv" | "unknown" => {
switch (type) {
case "movie":
return "movie";
case "tv":
case "show":
case "season":
case "episode":
return "tv";
default:
return "unknown";

View File

@@ -0,0 +1,375 @@
import dayjs from "dayjs";
import z from "zod";
import { createId } from "@homarr/common";
import { RequestError, ResponseError } from "@homarr/common/server";
import { logger } from "@homarr/log";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { ISystemHealthMonitoringIntegration } from "../interfaces/health-monitoring/health-monitoring-integration";
import type { SystemHealthMonitoring } from "../interfaces/health-monitoring/health-monitoring-types";
const localLogger = logger.child({ module: "TrueNasIntegration" });
const NETWORK_MULTIPLIER = 100;
export class TrueNasIntegration extends Integration implements ISystemHealthMonitoringIntegration {
private static webSocketMap = new Map<string, WebSocket>();
private wsUrl() {
const url = super.url("/websocket");
url.protocol = url.protocol.replace("http", "ws");
return url;
}
private get webSocket() {
return TrueNasIntegration.webSocketMap.get(this.integration.id) ?? null;
}
protected async testingAsync(_input: IntegrationTestingInput): Promise<TestingResult> {
const webSocket = await this.connectWebSocketAsync();
await this.registerSessionAsync(webSocket);
await this.authenticateWebSocketAsync(webSocket);
// Remove current socket connection so we can authenticate with updated credentials
TrueNasIntegration.webSocketMap.delete(this.integration.id);
return { success: true };
}
/**
* TrueNAS API uses WebSocket. This function connects to the socket
* and resolves the promise if the connection was successful.
* @see https://www.truenas.com/docs/api/scale_websocket_api.html
*/
private async connectWebSocketAsync(): Promise<WebSocket> {
localLogger.debug("Connecting to websocket server", {
url: this.wsUrl(),
});
const webSocket = new WebSocket(this.wsUrl());
return new Promise((resolve, reject) => {
webSocket.onopen = () => {
localLogger.debug("Connected to websocket server", {
url: this.wsUrl(),
});
resolve(webSocket);
};
webSocket.onerror = () => {
reject(new Error("Failed to connect"));
};
});
}
/**
* Before authentication, a session must be obtained from the server using the "connect" event.
* @see https://www.truenas.com/docs/api/scale_websocket_api.html#websocket_protocol
*/
private async registerSessionAsync(webSocket: WebSocket): Promise<void> {
return new Promise((resolve, reject) => {
const subscribe = (event: MessageEvent<string>) => {
const data = JSON.parse(event.data) as { msg: string };
if (data.msg === "connected") {
webSocket.removeEventListener("message", subscribe);
resolve();
} else if (data.msg === "failed") {
webSocket.removeEventListener("message", subscribe);
reject(new Error("Unable to establish connection"));
}
};
webSocket.addEventListener("message", subscribe);
webSocket.send(
JSON.stringify({
msg: "connect",
version: "1", // this must be number, not string
support: ["1"], // this must be number, not string
}),
);
});
}
/**
* After a session was obtained, the session can be authenticated.
* @see https://www.truenas.com/docs/api/scale_websocket_api.html#websocket_protocol
*/
private async authenticateWebSocketAsync(webSocket?: WebSocket): Promise<void> {
localLogger.debug("Authenticating with username and password", {
url: this.wsUrl(),
});
const response = await this.requestAsync(
"auth.login",
[this.getSecretValue("username"), this.getSecretValue("password")],
webSocket,
);
const result = await z.boolean().parseAsync(response);
if (!result) throw new ResponseError({ status: 401 });
localLogger.debug("Authenticated successfully with username and password", {
url: this.wsUrl(),
});
}
/**
* Retrieves data using the reporting method
* @see https://www.truenas.com/docs/api/scale_websocket_api.html#reporting
*/
private async getReportingAsync(): Promise<ReportingItem[]> {
localLogger.debug("Retrieving reporting data", {
url: this.wsUrl(),
});
const response = await this.requestAsync("reporting.get_data", [
[
{
name: "cpu",
},
{
name: "memory",
},
{
name: "cputemp",
},
],
{
aggregate: true,
start: dayjs().add(-5, "minutes").unix(),
end: dayjs().unix(),
},
]);
const result = await z.array(reportingItemSchema).parseAsync(response);
localLogger.debug("Retrieved reporting data", {
url: this.wsUrl(),
count: result.length,
});
return result;
}
/**
* Retrieves a list of all available network interfaces
* @see https://www.truenas.com/docs/core/13.0/api/core_websocket_api.html#interface
*/
private async getNetworkInterfacesAsync(): Promise<z.infer<typeof networkInterfaceSchema>> {
localLogger.debug("Retrieving available network-interfaces", {
url: this.wsUrl(),
});
const response = await this.requestAsync("interface.query", [
[], // no filters
{},
]);
const result = await networkInterfaceSchema.parseAsync(response);
localLogger.debug("Retrieved available network-interfaces", {
url: this.wsUrl(),
count: result.length,
});
return result;
}
/**
* Retrieves reporting network data of the last 5 minutes
* @see https://www.truenas.com/docs/api/scale_websocket_api.html#reporting
*/
private async getReportingNetdataAsync(): Promise<z.infer<typeof reportingNetDataSchema>> {
const networkInterfaces = await this.getNetworkInterfacesAsync();
localLogger.debug("Retrieving reporting network data", {
url: this.wsUrl(),
});
const response = await this.requestAsync("reporting.netdata_get_data", [
networkInterfaces.map((networkInterface) => ({
name: "interface",
identifier: networkInterface.id,
})),
{
start: dayjs().add(-5, "minutes").unix(),
end: dayjs().unix(),
},
]);
const result = await reportingNetDataSchema.parseAsync(response);
localLogger.debug("Retrieved reporting-network-data", {
url: this.wsUrl(),
count: result.length,
});
return result;
}
/**
* Retrieves information about the system
* @see https://www.truenas.com/docs/api/scale_websocket_api.html#system
*/
private async getSystemInformationAsync(): Promise<z.infer<typeof systemInfoSchema>> {
localLogger.debug("Retrieving system-information", {
url: this.wsUrl(),
});
const response = await this.requestAsync("system.info");
const result = await systemInfoSchema.parseAsync(response);
localLogger.debug("Retrieved system-information", {
url: this.wsUrl(),
});
return result;
}
public async getSystemInfoAsync(): Promise<SystemHealthMonitoring> {
const systemInformation = await this.getSystemInformationAsync();
const reporting = await this.getReportingAsync();
const cpuData = this.extractLatestReportingData(reporting, "cpu");
const cpuTempData = this.extractLatestReportingData(reporting, "cputemp");
const memoryData = this.extractLatestReportingData(reporting, "memory");
const netdata = await this.getReportingNetdataAsync();
const upload = this.extractNetworkTrafficData(netdata, 2); // Index 2 is "sent"
const download = this.extractNetworkTrafficData(netdata, 1); // Index 1 is "received"
return {
cpuUtilization: cpuData.reduce((acc, item) => acc + (item > 100 ? 0 : item), 0) / cpuData.length,
cpuTemp: Math.max(...cpuTempData.filter((_item, i) => i > 0)),
memAvailableInBytes: systemInformation.physmem,
memUsedInBytes: memoryData[1] ?? 0, // Index 0 is UNIX timestamp, Index 1 is free space in bytes
fileSystem: [],
availablePkgUpdates: 0,
network: {
up: upload * NETWORK_MULTIPLIER,
down: download * NETWORK_MULTIPLIER,
},
loadAverage: null,
smart: [],
uptime: systemInformation.uptime_seconds,
version: systemInformation.version,
cpuModelName: systemInformation.model,
rebootRequired: false,
};
}
/**
* Send a request through websocket and return response
* Times out after 5 seconds when no response was received.
* @param method json-rpc method to call
* @param params array of parameters
* @param webSocketOverride override of webSocket, helpful for not storing the connection
* @returns result of json-rpc call
*/
private async requestAsync(method: string, params: unknown[] = [], webSocketOverride?: WebSocket) {
let webSocket = webSocketOverride ?? this.webSocket;
if (!webSocket || webSocket.readyState !== WebSocket.OPEN) {
localLogger.debug("Connecting to websocket", {
url: this.wsUrl(),
});
// We can only land here with static webSocket
webSocket = await this.connectWebSocketAsync();
await this.registerSessionAsync(webSocket);
TrueNasIntegration.webSocketMap.set(this.integration.id, webSocket);
await this.authenticateWebSocketAsync();
}
return await new Promise((resolve, reject) => {
const id = createId();
const handler = (event: MessageEvent<string>) => {
const data = JSON.parse(event.data) as Record<string, unknown>;
if (data.msg !== "result") return;
if (data.id !== id) return;
clearTimeout(timeoutId);
webSocket.removeEventListener("message", handler);
localLogger.debug("Received method response", {
id,
method,
url: this.wsUrl(),
});
resolve(data.result);
};
const timeoutId = setTimeout(() => {
webSocket.removeEventListener("message", handler);
reject(
new RequestError(
{
type: "timeout",
reason: "aborted",
code: "ECONNABORTED",
},
{ cause: new Error("Canceled request after 5 seconds") },
),
);
}, 5000);
webSocket.addEventListener("message", handler);
localLogger.debug("Sending method request", {
id,
method,
url: this.wsUrl(),
});
webSocket.send(
JSON.stringify({
id,
msg: "method",
method,
params,
}),
);
});
}
private extractNetworkTrafficData = (data: z.infer<typeof reportingNetDataSchema>, index = 1 | 2) => {
return data.reduce((acc, current) => acc + (current.data.at(-1)?.at(index) ?? 0), 0);
};
private extractLatestReportingData(data: ReportingItem[], key: ReportingItem["identifier"]) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const dataObject = data.find((item) => item.identifier === key)!;
// TODO: check why the below sorting is done, because right now it compares number[] with number[]?
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return dataObject.data.sort((item1, item2) => (item1 > item2 ? -1 : 1))[0]!;
}
}
const reportingItemSchema = z.object({
name: z.enum(["cpu", "memory", "cputemp"]),
identifier: z.enum(["cpu", "memory", "cputemp"]),
aggregations: z.object({
min: z.record(z.string(), z.unknown()),
mean: z.record(z.string(), z.unknown()),
max: z.record(z.string(), z.unknown()),
}),
start: z.number().min(0),
end: z.number().min(0),
legend: z.array(z.string()),
data: z.array(z.array(z.number())),
});
type ReportingItem = z.infer<typeof reportingItemSchema>;
const reportingNetDataSchema = z.array(
z.object({
name: z.string(),
identifier: z.string(),
data: z.array(z.array(z.number())),
}),
);
const systemInfoSchema = z.object({
version: z.string(),
hostname: z.string(),
physmem: z.number().min(0), // pysical memory
model: z.string(), // cpu model
uptime_seconds: z.number().min(0),
});
const networkInterfaceSchema = z.array(
z.object({
id: z.string(),
name: z.string(),
}),
);

View File

@@ -33,7 +33,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -45,7 +45,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -59,7 +59,7 @@ export const ImportBoardModal = createModal(({ actions }) => {
}
setFileValid(true);
form.setFieldValue("configuration.name", result.data.configProperties.name);
form.setFieldValue("configuration.name", result.data.configProperties.name.replaceAll(" ", "-"));
})();
},
},

View File

@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -31,7 +31,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -52,7 +52,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/adm-zip": "0.5.7",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -29,7 +29,7 @@ export const analyseOldmarrImportAsync = async (file: File) => {
}
return {
name: entry.name.replace(".json", ""),
name: entry.name.replaceAll(" ", "-").replace(".json", ""),
config: result.data ?? null,
isError: !result.success,
};

View File

@@ -98,7 +98,6 @@ const optionMapping: OptionMapping = {
allowMicrophone: (oldOptions) => oldOptions.allowMicrophone,
allowGeolocation: (oldOptions) => oldOptions.allowGeolocation,
allowScrolling: (oldOptions) => oldOptions.allowScrolling,
allowTransparency: (oldOptions) => oldOptions.allowTransparency,
},
video: {
feedUrl: (oldOptions) => oldOptions.FeedUrl,

View File

@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -30,7 +30,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

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",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -22,7 +22,7 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@extractus/feed-extractor": "7.1.6",
"@extractus/feed-extractor": "7.1.7",
"@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
@@ -38,7 +38,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -35,7 +35,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -47,7 +47,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -33,7 +33,7 @@
"deepmerge": "4.3.1",
"mantine-react-table": "2.0.0-beta.9",
"next": "15.5.2",
"next-intl": "4.3.5",
"next-intl": "4.3.7",
"react": "19.1.1",
"react-dom": "19.1.1"
},
@@ -41,7 +41,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"typescript": "^5.9.2"
}
}

View File

@@ -44,7 +44,7 @@
"apps": "Aplicacions",
"boards": "Tauler",
"integrations": "Integracions",
"credentialUsers": ""
"credentialUsers": "Credencials d'usuari"
}
},
"tokenModal": {
@@ -82,7 +82,7 @@
"form": {
"name": {
"label": "Nom del grup",
"description": ""
"description": "El nom ha de coincidir amb el grup d'Administradors del proveïdor extern"
}
}
},
@@ -110,12 +110,12 @@
"page": {
"login": {
"title": "Entreu al vostre compte",
"subtitle": ""
"subtitle": "Benvingut de nou! Si us plau, introduïu les credencials"
},
"invite": {
"title": "Uneix-te a Homarr",
"subtitle": "",
"description": ""
"subtitle": "Benvingut a Homarr! Si us plau, crea el teu compte",
"description": "Has sigut invitat per {username}"
},
"init": {
"title": "Nova instal·lació de Homarr",
@@ -178,7 +178,7 @@
},
"forgotPassword": {
"label": "Heu oblidat la contrasenya?",
"description": ""
"description": "Un administrador pot utilitzar el següent comandament per reinicialitzar la teva contrasenya"
}
},
"register": {
@@ -239,10 +239,10 @@
"changePingIconsEnabled": {
"notification": {
"success": {
"message": ""
"message": "Icones de ping alternats correctament"
},
"error": {
"message": ""
"message": "Impossible d'alternar les icones de ping"
}
}
},
@@ -307,12 +307,12 @@
"name": "Nom",
"members": "Membres",
"homeBoard": {
"label": "",
"description": ""
"label": "Panell d'inici",
"description": "Només els panells accessibles al grup es pot seleccionar"
},
"mobileBoard": {
"label": "",
"description": ""
"label": "Panell mòbil",
"description": "Només els panells accessibles al grup es pot seleccionar"
}
},
"permission": {
@@ -384,29 +384,29 @@
},
"full-all": {
"label": "Accés total a les integracions",
"description": ""
"description": "Permetre que els membres gestionin, utilitzin i interactuïn amb cada integració"
}
}
},
"media": {
"title": "",
"title": "Medis",
"item": {
"upload": {
"label": "",
"description": ""
"label": "Pujar medis",
"description": "Permetre als membres puja medis"
},
"view-all": {
"label": "",
"description": ""
"label": "Veure medis",
"description": "Permet que els membres vegin tots els continguts multimèdia"
},
"full-all": {
"label": "",
"description": ""
"label": "Accés complet als medis",
"description": "Permet als membres gestionar i esborrar tots els continguts multimèdia"
}
}
},
"other": {
"title": "",
"title": "Altres",
"item": {
"view-logs": {
"label": "Veure registres",
@@ -426,95 +426,95 @@
"description": "Permet que els membres modifiquin tots els motors de cerca"
},
"full-all": {
"label": "",
"description": ""
"label": "Accés complet a la cerca",
"description": "Permet als membres gestionar i esborrar els motors de cerca"
}
}
}
},
"memberNotice": {
"mixed": "",
"external": ""
"mixed": "Alguns membres de proveïdors externs no es poden gestionar aquí",
"external": "Tots els membres de proveïdors externs no es poden gestionar aquí"
},
"reservedNotice": {
"message": ""
"message": "Aquest grup està reservat pel sistema i restringeix algunes accions <checkoutDocs></checkoutDocs>"
},
"action": {
"create": {
"label": "",
"label": "Nou grup",
"notification": {
"success": {
"message": ""
"message": "El grup s'ha creat satisfactòriament"
},
"error": {
"message": ""
"message": "El grup no es pot crear"
}
}
},
"transfer": {
"label": "",
"description": "",
"confirm": "",
"label": "Transferir propietari",
"description": "Transferir la propietat del grup a altre usuari.",
"confirm": "Segur que vols transferir el propietari de {name} a {username}?",
"notification": {
"success": {
"message": ""
"message": "Transferència del {group} a {user} satisfactòria"
},
"error": {
"message": ""
"message": "No ha sigut possible transferir la propietat"
}
}
},
"addMember": {
"label": ""
"label": "Afegir usuari"
},
"removeMember": {
"label": "",
"confirm": ""
"label": "Esborrar usuari",
"confirm": "Segur que vols esborrar {user} d'aquest grup?"
},
"delete": {
"label": "",
"description": "",
"confirm": "",
"label": "Esborrar grup",
"description": "Un cop esborris el grup, no el podràs recuperar. ",
"confirm": "Segur que desitges esborrar el grup {name}?",
"notification": {
"success": {
"message": ""
"message": "S'ha esborrat el grup {name}"
},
"error": {
"message": ""
"message": "Impossible esborrar el grup {name}"
}
}
},
"changePermissions": {
"notification": {
"success": {
"title": "",
"message": ""
"title": "Privilegis guardats",
"message": "Els privilegis s'han guardat satisfactòriament"
},
"error": {
"title": "",
"message": ""
"title": "Privilegis no guardats",
"message": "Els privilegis no s'han guardat"
}
}
},
"update": {
"notification": {
"success": {
"message": ""
"message": "El grup {name} s'ha guardat satisfactòriament"
},
"error": {
"message": ""
"message": "No s'ha pogut guardar el grup {name}"
}
}
},
"select": {
"label": "",
"notFound": ""
"label": "Seleccionar grup",
"notFound": "No s'ha trobat el grup"
},
"settings": {
"board": {
"notification": {
"success": {
"title": "",
"title": "Configuració guardada",
"message": ""
},
"error": {
@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "",
"newLabel": ""
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "",
"newLabel": ""
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "",
"newLabel": ""
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "API-nøgle (Secret)",
"newLabel": "Ny API-nøgle (Key)"
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "",
"newLabel": ""
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "",
"newLabel": ""
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "",
"newLabel": ""
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "",
"newLabel": ""
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "API Key (Secret)",
"newLabel": "New API Key (Secret)"
},
"githubAppId": {
"label": "App Id",
"newLabel": "New App Id"
},
"githubInstallationId": {
"label": "Installation Id",
"newLabel": "New Installation Id"
},
"privateKey": {
"label": "Private key",
"newLabel": "New private key"
}
}
},
@@ -3243,9 +3255,6 @@
"dnsHole": {
"label": "DNS Hole Data"
},
"sessionCleanup": {
"label": "Session Cleanup"
},
"updateChecker": {
"label": "Update checker"
},

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "Clave API (Credencial)",
"newLabel": "Nueva clave API (Credencial)"
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},
@@ -1279,12 +1291,12 @@
}
},
"descriptionDisplayMode": {
"label": "",
"description": "",
"label": "Modo de visualización de descripción",
"description": "Elige cómo mostrar la descripción de la aplicación",
"option": {
"normal": "",
"tooltip": "",
"hidden": ""
"normal": "Dentro del widget",
"tooltip": "Como consejo",
"hidden": "Oculto"
}
}
},
@@ -2261,7 +2273,7 @@
"label": "Cantidad límite de publicaciones"
},
"hideDescription": {
"label": ""
"label": "Ocultar descripción"
}
}
},
@@ -3337,22 +3349,22 @@
},
"disable": {
"success": {
"message": ""
"message": "Tarea desactivada con éxito"
}
},
"toggle": {
"error": {
"message": ""
"message": "Error al cambiar el estado de la tarea"
}
}
},
"api": {
"title": "",
"title": "API",
"modal": {
"createApiToken": {
"title": "",
"description": "",
"button": ""
"title": "Token de API creado",
"description": "El token de API fue creado. Ten cuidado, este token está cifrado en la base de datos y nunca será transferido de nuevo a ti. Si pierdes este token, ya no podrás recuperar este token específico.",
"button": "Copiar y cerrar"
}
},
"tab": {
@@ -3360,22 +3372,22 @@
"label": "Documentación"
},
"apiKey": {
"label": "",
"title": "",
"label": "Autenticación",
"title": "Claves API",
"button": {
"createApiToken": ""
"createApiToken": "Crear token de API"
},
"modal": {
"delete": {
"title": "",
"text": ""
"title": "Eliminar token de API",
"text": "Esto eliminará permanentemente el token de la API. Los clientes de la API que utilicen este token ya no pueden autenticarse y realizar solicitudes de API. Esta acción no se puede deshacer."
}
},
"table": {
"header": {
"id": "",
"createdBy": "",
"actions": ""
"id": "ID",
"createdBy": "Creado por",
"actions": "Acciones"
}
}
}
@@ -3383,20 +3395,20 @@
}
},
"about": {
"version": "",
"text": "",
"version": "Versión {version}",
"text": "Homarr es un proyecto de código abierto impulsado por la comunidad que está siendo mantenido por voluntarios. Gracias a estas personas, Homarr ha sido un proyecto creciente desde 2021. Nuestro equipo trabaja de forma remota en Homarr desde muchos países en su tiempo libre, sin compensación.",
"accordion": {
"contributors": {
"title": "",
"subtitle": ""
"title": "Colaboradores",
"subtitle": "{count} manteniendo el código y Homarr"
},
"translators": {
"title": "",
"subtitle": ""
"title": "Traductores",
"subtitle": "{count} contribuyendo con las traducciones en muchos idiomas"
},
"libraries": {
"title": "",
"subtitle": ""
"title": "Librerías",
"subtitle": "{count} usadas en el Código de Homarr"
}
}
}
@@ -3405,10 +3417,10 @@
"docker": {
"title": "Contenedores",
"table": {
"updated": "",
"search": "",
"selected": "",
"footer": ""
"updated": "Actualizado {when}",
"search": "Buscar {count} contenedores",
"selected": "{selectCount} de {totalCount} contenedores seleccionados",
"footer": "{count} contenedores en total"
},
"field": {
"name": {
@@ -3421,17 +3433,17 @@
"running": "En ejecución",
"paused": "Pausado",
"restarting": "Reiniciando",
"exited": "",
"exited": "Finalizado",
"removing": "Eliminando",
"dead": ""
"dead": "Caido"
}
},
"stats": {
"cpu": {
"label": ""
"label": "CPU"
},
"memory": {
"label": ""
"label": "Memoria"
}
},
"containerImage": {
@@ -3442,17 +3454,17 @@
}
},
"action": {
"title": "",
"title": "Acciones",
"start": {
"label": "Iniciar",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Contenedores iniciados",
"message": "Los contenedores se iniciaron con éxito"
},
"error": {
"title": "",
"message": ""
"title": "Contenedores no iniciados",
"message": "Los contenedores no se han podido iniciar"
}
}
},
@@ -3460,12 +3472,12 @@
"label": "Detener",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Contenedores detenidos",
"message": "Los contenedores se detuvieron con éxito"
},
"error": {
"title": "",
"message": ""
"title": "Contenedores no detenidos",
"message": "Los contenedores no pudieron ser detenidos"
}
}
},
@@ -3473,12 +3485,12 @@
"label": "Reiniciar",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Contenedores reiniciados",
"message": "Los contenedores se reiniciaron con éxito"
},
"error": {
"title": "",
"message": ""
"title": "No se reiniciaron los contenedores",
"message": "Los contenedores no se pudieron reiniciar"
}
}
},
@@ -3486,316 +3498,316 @@
"label": "Eliminar",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Contenedores eliminados",
"message": "Los contenedores fueron eliminados con éxito"
},
"error": {
"title": "",
"message": ""
"title": "Contenedores no eliminados",
"message": "Los contenedores no pudieron ser eliminados"
}
}
},
"refresh": {
"label": "",
"label": "Actualizar",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Contenedores actualizados",
"message": "Ahora estás viendo los datos más recientes"
},
"error": {
"title": "",
"message": ""
"title": "Contenedores no actualizados",
"message": "Algo salió mal mientras se actualizaban los contenedores"
}
}
},
"addToHomarr": {
"label": "",
"label": "Añadir a Homarr",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Añadido a Homarr",
"message": "Las aplicaciones seleccionadas han sido añadidas a Homarr"
},
"error": {
"title": "",
"message": ""
"title": "No se pudo añadir a Homarr",
"message": "Las aplicaciones seleccionadas no se han podido añadir a Homarr"
}
},
"modal": {
"title": ""
"title": "Añadir contenedor(es) docker a Homarr"
}
}
},
"error": {
"internalServerError": ""
"internalServerError": "Fallo al obtener contenedores Docker"
}
},
"kubernetes": {
"cluster": {
"title": "",
"label": "",
"providers": "",
"version": "",
"architecture": "",
"title": "Panel del Clúster",
"label": "Clúster",
"providers": "Proveedores",
"version": "Versión",
"architecture": "Arquitectura",
"capacity": {
"title": "",
"title": "Capacidad",
"resource": {
"reserved": "",
"used": ""
"reserved": "Reservado",
"used": "Usado"
}
},
"resources": {
"title": "",
"nodes": "",
"namespaces": "",
"ingresses": "",
"services": "",
"pods": "",
"configmaps": "",
"secrets": "",
"volumes": ""
"title": "Recursos",
"nodes": "Nodos",
"namespaces": "Espacios de nombres",
"ingresses": "Ingresos",
"services": "Servicios",
"pods": "Pods",
"configmaps": "ConfigMaps",
"secrets": "Credenciales",
"volumes": "Volúmenes"
}
},
"nodes": {
"label": "",
"label": "Nodos",
"field": {
"name": {
"label": ""
"label": "Nombre"
},
"state": {
"label": "",
"label": "Estado",
"option": {
"ready": "",
"NotReady": ""
"ready": "Listo",
"NotReady": "No está listo"
}
},
"cpu": {
"label": ""
"label": "CPU"
},
"memory": {
"label": ""
"label": "RAM"
},
"pods": {
"label": ""
"label": "Pods"
},
"operatingSystem": {
"label": ""
"label": "Sistema Operativo"
},
"architecture": {
"label": ""
"label": "Arquitectura"
},
"kubernetesVersion": {
"label": ""
"label": "Versión de Kubernetes"
},
"creationTimestamp": {
"label": ""
"label": "Creado"
}
},
"table": {
"search": ""
"search": "Buscar {count} nodos"
}
},
"namespaces": {
"label": "",
"label": "Espacios de nombres",
"field": {
"name": {
"label": ""
"label": "Nombre"
},
"state": {
"label": "",
"label": "Estado",
"option": {
"active": "",
"terminating": ""
"active": "Activo",
"terminating": "Terminando"
}
},
"creationTimestamp": {
"label": ""
"label": "Creado"
}
},
"table": {
"search": ""
"search": "Buscar {count} espacios de nombres"
}
},
"ingresses": {
"label": "",
"label": "Ingresos",
"field": {
"name": {
"label": ""
"label": "Nombre"
},
"namespace": {
"label": ""
"label": "Espacio de nombre"
},
"className": {
"label": ""
"label": "Nombre de la clase"
},
"rulesAndPaths": {
"label": ""
"label": "Reglas y rutas"
},
"creationTimestamp": {
"label": ""
"label": "Creado"
}
},
"table": {
"search": ""
"search": "Buscar {count} ingresos"
}
},
"services": {
"label": "",
"label": "Servicios",
"field": {
"name": {
"label": ""
"label": "Nombre"
},
"namespace": {
"label": ""
"label": "Espacio de nombre"
},
"type": {
"label": ""
"label": "Tipo"
},
"ports": {
"label": ""
"label": "Puertos"
},
"targetPorts": {
"label": ""
"label": "Puertos de destino"
},
"clusterIP": {
"label": ""
"label": "IP del clúster"
},
"creationTimestamp": {
"label": ""
"label": "Creado"
}
},
"table": {
"search": ""
"search": "Buscar {count} servicios"
}
},
"pods": {
"label": "",
"label": "Pods",
"field": {
"name": {
"label": ""
"label": "Nombre"
},
"namespace": {
"label": ""
"label": "Espacio de nombre"
},
"image": {
"label": ""
"label": "Imagen"
},
"applicationType": {
"label": ""
"label": "Tipo de aplicación"
},
"status": {
"label": ""
"label": "Estado"
},
"creationTimestamp": {
"label": ""
"label": "Creado"
}
},
"table": {
"search": ""
"search": "Buscar {count} pods"
}
},
"secrets": {
"label": "",
"label": "Credenciales",
"field": {
"name": {
"label": ""
"label": "Nombre"
},
"namespace": {
"label": ""
"label": "Espacio de nombre"
},
"type": {
"label": ""
"label": "Tipo"
},
"creationTimestamp": {
"label": ""
"label": "Creado"
}
},
"table": {
"search": ""
"search": "Buscar {count} credenciales"
}
},
"configmaps": {
"label": "",
"label": "ConfigMaps",
"field": {
"name": {
"label": ""
"label": "Nombre"
},
"namespace": {
"label": ""
"label": "Espacio de nombre"
},
"creationTimestamp": {
"label": ""
"label": "Creado"
}
},
"table": {
"search": ""
"search": "Buscar {count} configMaps"
}
},
"volumes": {
"label": "",
"label": "Volúmenes",
"field": {
"name": {
"label": ""
"label": "Nombre"
},
"namespace": {
"label": ""
"label": "Espacio de nombre"
},
"accessModes": {
"label": ""
"label": "Modos de acceso"
},
"storage": {
"label": ""
"label": "Almacenamiento"
},
"storageClassName": {
"label": ""
"label": "Nombre de la clase de almacenamiento"
},
"volumeMode": {
"label": ""
"label": "Modo del volumen"
},
"volumeName": {
"label": ""
"label": "Nombre del volumen"
},
"status": {
"label": ""
"label": "Estado"
},
"creationTimestamp": {
"label": ""
"label": "Creado"
}
},
"table": {
"search": ""
"search": "Buscar {count} volúmenes"
}
},
"error": {
"internalServerError": ""
"internalServerError": "Error al recuperar datos de Kubernetes"
}
},
"permission": {
"title": "",
"title": "Permisos",
"userSelect": {
"title": ""
"title": "Añadir permisos de usuario"
},
"groupSelect": {
"title": ""
"title": "Añadir permisos de grupo"
},
"tab": {
"user": "Usuarios",
"group": "",
"inherited": ""
"group": "Grupos",
"inherited": "Grupos heredados"
},
"field": {
"user": {
"label": "Usuario"
},
"group": {
"label": ""
"label": "Grupo"
},
"permission": {
"label": ""
"label": "Permisos"
}
},
"action": {
"saveUser": "",
"saveGroup": ""
"saveUser": "Guardar permisos de usuario",
"saveGroup": "Guardar permisos de grupo"
}
},
"navigationStructure": {
@@ -3805,30 +3817,30 @@
"label": "Tableros"
},
"integrations": {
"label": "",
"label": "Integraciones",
"edit": {
"label": "Editar"
},
"new": {
"label": ""
"label": "Nueva"
}
},
"search-engines": {
"label": "",
"label": "Motores de búsqueda",
"new": {
"label": ""
"label": "Nuevo"
},
"edit": {
"label": "Editar"
}
},
"medias": {
"label": ""
"label": "Imágenes"
},
"apps": {
"label": "Aplicaciones",
"new": {
"label": ""
"label": "Nueva"
},
"edit": {
"label": "Editar"
@@ -3839,11 +3851,11 @@
"create": {
"label": "Crear"
},
"general": "",
"general": "General",
"security": "Seguridad",
"board": "Tableros",
"groups": {
"label": ""
"label": "Grupos"
},
"invites": {
"label": "Invitaciones"
@@ -3852,33 +3864,33 @@
"tools": {
"label": "Herramientas",
"tasks": {
"label": ""
"label": "Tareas"
},
"docker": {
"label": ""
"label": "Docker"
},
"kubernetes": {
"label": "",
"label": "Kubernetes",
"nodes": {
"label": ""
"label": "Nodos"
},
"namespaces": {
"label": ""
"label": "Espacios de nombres"
},
"ingresses": {
"label": ""
"label": "Ingresos"
},
"services": {
"label": ""
"label": "Servicios"
},
"pods": {
"label": ""
"label": "Pods"
},
"configmaps": {
"label": ""
"label": "ConfigMaps"
},
"secrets": {
"label": ""
"label": "Credenciales"
},
"volumes": {
"label": ""
@@ -4069,7 +4081,7 @@
"label": ""
},
"discord": {
"label": "Comunidad Discord"
"label": "Comunidad de Discord"
}
}
}

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "",
"newLabel": ""
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "",
"newLabel": ""
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "",
"newLabel": ""
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "",
"newLabel": ""
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "",
"newLabel": ""
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "",
"newLabel": ""
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "API キー (シークレット)",
"newLabel": "新しい API キー (シークレット)"
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "",
"newLabel": ""
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "",
"newLabel": ""
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "",
"newLabel": ""
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "",
"newLabel": ""
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "",
"newLabel": ""
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},

View File

@@ -789,7 +789,7 @@
},
"notification": {
"success": {
"title": "Zaufane certyfikaty",
"title": "Zaufany certyfikat",
"message": "Dodano nazwę hosta do listy zaufanych certyfikatów"
},
"error": {
@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "Klucz API (Secret)",
"newLabel": "Nowy klucz API (Secret)"
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},
@@ -2261,7 +2273,7 @@
"label": "Limit liczby postów"
},
"hideDescription": {
"label": ""
"label": "Ukryj opis"
}
}
},
@@ -2722,7 +2734,7 @@
"layout": {
"title": "Układ",
"responsive": {
"title": "Responsywne układ",
"title": "Responsywne układy",
"action": {
"add": "Dodaj układ"
}
@@ -3740,16 +3752,16 @@
"label": "Przestrzeń nazw"
},
"accessModes": {
"label": ""
"label": "Tryby dostępu"
},
"storage": {
"label": "Pamięć"
},
"storageClassName": {
"label": ""
"label": "Nazwa klasy pamięci"
},
"volumeMode": {
"label": ""
"label": "Tryb woluminu"
},
"volumeName": {
"label": "Nazwa wolumenu"
@@ -3762,25 +3774,25 @@
}
},
"table": {
"search": ""
"search": "Szukaj {count} woluminów"
}
},
"error": {
"internalServerError": ""
"internalServerError": "Nie udało się pobrać danych Kubernetes"
}
},
"permission": {
"title": "Uprawnienia",
"userSelect": {
"title": ""
"title": "Dodaj uprawnienie użytkownika"
},
"groupSelect": {
"title": ""
"title": "Dodaj uprawnienie grupy"
},
"tab": {
"user": "Użytkownicy",
"group": "Grupy",
"inherited": ""
"inherited": "Dziedziczone grupy"
},
"field": {
"user": {
@@ -3794,8 +3806,8 @@
}
},
"action": {
"saveUser": "",
"saveGroup": ""
"saveUser": "Zapisz uprawnienia użytkownika",
"saveGroup": "Zapisz uprawnienia grupy"
}
},
"navigationStructure": {
@@ -3805,7 +3817,7 @@
"label": "Tablice"
},
"integrations": {
"label": "",
"label": "Integracje",
"edit": {
"label": "Edytuj"
},
@@ -3814,7 +3826,7 @@
}
},
"search-engines": {
"label": "",
"label": "Wyszukiwarki",
"new": {
"label": "Nowy"
},
@@ -3903,28 +3915,28 @@
}
},
"search": {
"placeholder": "",
"nothingFound": "",
"placeholder": "Wyszukaj cokolwiek",
"nothingFound": "Nic nie znaleziono",
"error": {
"fetch": ""
"fetch": "Wystąpił błąd podczas pobierania danych"
},
"mode": {
"appIntegrationBoard": {
"help": "",
"help": "Szukaj aplikacji, integracji lub tablic",
"group": {
"app": {
"title": "Aplikacje",
"children": {
"action": {
"open": {
"label": ""
"label": "Otwórz adres URL aplikacji"
},
"edit": {
"label": "Edytuj aplikację"
}
},
"detail": {
"title": ""
"title": "Wybierz akcję dla aplikacji"
}
}
},
@@ -3936,60 +3948,60 @@
"label": "Otwórz tablicę"
},
"homeBoard": {
"label": ""
"label": "Ustaw jako tablicę główną"
},
"mobileBoard": {
"label": ""
"label": "Ustaw jako tablicę mobilną"
},
"settings": {
"label": ""
"label": "Otwórz ustawienia"
}
},
"detail": {
"title": ""
"title": "Wybierz akcję dla tablicy"
}
}
},
"integration": {
"title": ""
"title": "Integracje"
}
}
},
"command": {
"help": "",
"help": "Aktywuj tryb poleceń",
"group": {
"localCommand": {
"title": ""
"title": "Polecenia lokalne"
},
"globalCommand": {
"title": "",
"title": "Polecenia globalne",
"option": {
"colorScheme": {
"light": "",
"dark": ""
"light": "Przełącz na tryb jasny",
"dark": "Przełącz na tryb ciemny"
},
"language": {
"label": "Zmień język",
"children": {
"detail": {
"title": ""
"title": "Wybierz preferowany język"
}
}
},
"newBoard": {
"label": ""
"label": "Utwórz nową tablicę"
},
"importBoard": {
"label": ""
"label": "Importuj tablicę"
},
"newApp": {
"label": ""
"label": "Utwórz nową aplikację"
},
"newIntegration": {
"label": "",
"label": "Utwórz nową integrację",
"children": {
"detail": {
"title": ""
"title": "Wybierz typ integracji, który chcesz utworzyć"
}
}
},
@@ -4007,48 +4019,48 @@
}
},
"media": {
"requestMovie": "",
"requestSeries": "",
"openIn": ""
"requestMovie": "Poproś o film",
"requestSeries": "Poproś o serial",
"openIn": "Otwórz w {kind}"
},
"external": {
"help": "",
"help": "Użyj zewnętrznej wyszukiwarki",
"group": {
"searchEngine": {
"title": "",
"title": "Wyszukiwarki",
"children": {
"action": {
"search": {
"label": ""
"label": "Wyszukaj za pomocą {name}"
}
},
"detail": {
"title": ""
"title": "Wybierz akcję dla wyszukiwarki"
},
"searchResults": {
"title": ""
"title": "Wybierz wynik wyszukiwania do wykonania akcji"
}
},
"option": {
"google": {
"name": "Google",
"description": ""
"description": "Przeszukaj internet za pomocą Google"
},
"bing": {
"name": "Bing",
"description": ""
"description": "Przeszukaj internet za pomocą Bing"
},
"duckduckgo": {
"name": "DuckDuckGo",
"description": ""
"description": "Przeszukaj internet za pomocą DuckDuckGo"
},
"torrent": {
"name": "Torrenty",
"description": ""
"description": "Wyszukaj torrenty na torrentdownloads.pro"
},
"youTube": {
"name": "YouTube",
"description": ""
"description": "Wyszukaj filmy na YouTube"
}
}
}
@@ -4057,7 +4069,7 @@
"help": {
"group": {
"mode": {
"title": ""
"title": "Tryby"
},
"help": {
"title": "Pomoc",
@@ -4066,7 +4078,7 @@
"label": "Dokumentacja"
},
"submitIssue": {
"label": ""
"label": "Zgłoś problem"
},
"discord": {
"label": "Discord społeczności"
@@ -4081,69 +4093,69 @@
"title": "Szukaj",
"option": {
"other": {
"label": ""
"label": "Wyszukaj za pomocą innej wyszukiwarki"
},
"no-default": {
"label": "",
"description": ""
"label": "Brak domyślnej wyszukiwarki",
"description": "Ustaw domyślną wyszukiwarkę w ustawieniach"
},
"search": {
"label": ""
"label": "Wyszukaj „{query}” za pomocą {name}"
},
"from-integration": {
"description": ""
"description": "Zacznij pisać, aby wyszukać"
}
}
},
"local": {
"title": ""
"title": "Wyniki lokalne"
}
}
},
"page": {
"help": "",
"help": "Wyszukaj strony",
"group": {
"page": {
"title": "Strony",
"option": {
"manageHome": {
"label": ""
"label": "Zarządzaj stroną główną"
},
"manageBoard": {
"label": ""
"label": "Zarządzaj tablicami"
},
"manageApp": {
"label": ""
"label": "Zarządzaj aplikacjami"
},
"manageIntegration": {
"label": ""
"label": "Zarządzaj integracjami"
},
"manageSearchEngine": {
"label": ""
"label": "Zarządzaj wyszukiwarkami"
},
"manageMedia": {
"label": ""
"label": "Zarządzaj mediami"
},
"manageUser": {
"label": "Zarządzaj użytkownikami"
},
"manageInvite": {
"label": ""
"label": "Zarządzaj zaproszeniami"
},
"manageGroup": {
"label": ""
"label": "Zarządzaj grupami"
},
"manageDocker": {
"label": ""
"label": "Zarządzaj Dockerem"
},
"manageApi": {
"label": ""
"label": "Swagger API"
},
"manageLog": {
"label": ""
"label": "Wyświetl logi"
},
"manageTask": {
"label": ""
"label": "Zarządzaj zadaniami"
},
"manageSettings": {
"label": "Ustawienia globalne"
@@ -4162,18 +4174,18 @@
}
},
"userGroup": {
"help": "",
"help": "Wyszukaj użytkowników lub grupy",
"group": {
"user": {
"title": "Użytkownicy",
"children": {
"action": {
"detail": {
"label": ""
"label": "Pokaż szczegóły użytkownika"
}
},
"detail": {
"title": ""
"title": "Wybierz akcję dla użytkownika"
}
}
},
@@ -4182,17 +4194,17 @@
"children": {
"action": {
"detail": {
"label": ""
"label": "Pokaż szczegóły grupy"
},
"manageMember": {
"label": ""
"label": "Zarządzaj użytkownikami"
},
"managePermission": {
"label": ""
"label": "Zarządzanie uprawnieniami"
}
},
"detail": {
"title": ""
"title": "Wybierz akcję dla grupy"
}
}
}
@@ -4200,72 +4212,72 @@
}
},
"engine": {
"search": "",
"search": "Znajdź wyszukiwarkę",
"field": {
"name": {
"label": "Nazwa"
},
"short": {
"label": ""
"label": "Krótki"
},
"urlTemplate": {
"label": ""
"label": "Szablon wyszukiwania URL"
},
"description": {
"label": ""
"label": "Opis"
}
},
"page": {
"list": {
"title": "",
"title": "Wyszukiwarki",
"noResults": {
"title": "",
"action": ""
"title": "Nie ma jeszcze żadnych wyszukiwarek",
"action": "Utwórz swoją pierwszą wyszukiwarkę"
},
"interactive": ""
"interactive": "Interaktywny, korzysta z integracji"
},
"create": {
"title": "",
"title": "Nowa wyszukiwarka",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Wyszukiwarka została utworzona",
"message": "Wyszukiwarka została pomyślnie utworzona"
},
"error": {
"title": "",
"message": ""
"title": "Wyszukiwarka nie została utworzona",
"message": "Nie udało się utworzyć wyszukiwarki"
}
}
},
"edit": {
"title": "",
"title": "Edytuj wyszukiwarkę",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Zmiany zostały pomyślnie zastosowane",
"message": "Wyszukiwarka została pomyślnie zapisana"
},
"error": {
"title": "",
"message": ""
"title": "Nie można zastosować zmian",
"message": "Nie udało się zapisać wyszukiwarki"
}
},
"configControl": "",
"configControl": "Konfiguracja",
"searchEngineType": {
"generic": "",
"fromIntegration": ""
"generic": "Ogólny",
"fromIntegration": "Z integracji"
}
},
"delete": {
"title": "",
"message": "",
"title": "Usuń wyszukiwarkę",
"message": "Czy na pewno chcesz usunąć wyszukiwarkę {name}?",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Wyszukiwarka została usunięta",
"message": "Wyszukiwarka została pomyślnie usunięta"
},
"error": {
"title": "",
"message": ""
"title": "Wyszukiwarka nie została usunięta",
"message": "Nie udało się usunąć wyszukiwarki"
}
}
}
@@ -4273,15 +4285,15 @@
"media": {
"request": {
"modal": {
"title": "",
"title": "Żądanie \"{name}\"",
"table": {
"header": {
"season": "",
"episodes": ""
"season": "Sezon",
"episodes": "Odcinki"
}
},
"button": {
"send": ""
"send": "Wyślij żądanie"
}
}
}
@@ -4291,89 +4303,89 @@
"certificate": {
"field": {
"hostname": {
"label": ""
"label": "Nazwa hosta"
},
"subject": {
"label": ""
"label": "Temat"
},
"issuer": {
"label": ""
"label": "Wystawca"
},
"validFrom": {
"label": ""
"label": "Ważny od"
},
"validTo": {
"label": ""
"label": "Ważny do"
},
"serialNumber": {
"label": ""
"label": "Numer seryjny"
},
"fingerprint": {
"label": ""
"label": "Odcisk palca"
}
},
"page": {
"list": {
"title": "",
"description": "",
"title": "Zaufane certyfikaty",
"description": "Używane przez Homarr do pobierania danych z integracji.",
"noResults": {
"title": ""
"title": "Brak certyfikatów"
},
"invalid": {
"title": "",
"description": ""
"title": "Nieprawidłowy certyfikat",
"description": "Nie udało się przetworzyć certyfikatu"
},
"expires": "",
"toHostnames": ""
"expires": "Wygasa {when}",
"toHostnames": "Zaufane nazwy hostów"
},
"hostnames": {
"title": "",
"description": "",
"title": "Zaufane nazwy hostów certyfikatów",
"description": "Niektóre certyfikaty nie zezwalają na użycie konkretnej domeny, z której Homarr wysyła żądania. Z tego powodu wszystkie zaufane hosty wraz z odciskami ich certyfikatów są używane, aby obejść te ograniczenia.",
"noResults": {
"title": ""
"title": "Nie dodano jeszcze żadnych nazw hostów"
},
"toCertificates": ""
"toCertificates": "Certyfikaty"
}
},
"action": {
"create": {
"label": "",
"label": "Dodaj certyfikat",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Certyfikat dodany",
"message": "Certyfikat został dodany pomyślnie"
},
"error": {
"title": "",
"message": ""
"title": "Nie udało się dodać certyfikatu",
"message": "Nie udało się dodać certyfikatu"
}
}
},
"remove": {
"label": "",
"confirm": "",
"label": "Usuń certyfikat",
"confirm": "Czy na pewno chcesz usunąć certyfikat?",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Certyfikat usunięty",
"message": "Certyfikat został pomyślnie usunięty"
},
"error": {
"title": "",
"message": ""
"title": "Certyfikat nie został usunięty",
"message": "Nie udało się usunąć certyfikatu"
}
}
},
"removeHostname": {
"label": "",
"confirm": "",
"label": "Usuń zaufaną nazwę hosta",
"confirm": "Czy na pewno chcesz usunąć tą zaufaną nazwę hosta? Może to spowodować, że niektóre integracje przestaną działać.",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Nazwa hosta usunięta",
"message": "Nazwa hosta została pomyślnie usunięta"
},
"error": {
"title": "",
"message": ""
"title": "Nazwa hosta nie została usunięta",
"message": "Nie udało się usunąć nazwy hosta"
}
}
}
@@ -4382,10 +4394,10 @@
"log": {
"level": {
"option": {
"debug": "",
"info": "",
"warn": "",
"error": ""
"debug": "Debugowanie",
"info": "Info",
"warn": "Ostrzeżenie",
"error": "Błąd"
}
}
}

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "",
"newLabel": ""
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "",
"newLabel": ""
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},

View File

@@ -952,6 +952,18 @@
"opnsenseApiSecret": {
"label": "",
"newLabel": ""
},
"githubAppId": {
"label": "",
"newLabel": ""
},
"githubInstallationId": {
"label": "",
"newLabel": ""
},
"privateKey": {
"label": "",
"newLabel": ""
}
}
},

Some files were not shown because too many files have changed in this diff Show More