Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e63c0b0e65 | ||
|
|
8a43bf47aa |
No files matched your search
@@ -28,10 +28,6 @@ DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
|
||||
# DB_PASSWORD='password'
|
||||
# DB_NAME='name-of-database'
|
||||
|
||||
# The below path can be used to store trusted certificates during development, it is not required and can be left empty.
|
||||
# If it is used, please use the full path to the directory where the certificates are stored.
|
||||
# LOCAL_CERTIFICATE_PATH='FULL_PATH_TO_CERTIFICATES'
|
||||
|
||||
TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
# Configure logging to use winston logger
|
||||
|
||||
@@ -6,11 +6,6 @@
|
||||
matchPackagePatterns: ["^@homarr/"],
|
||||
enabled: false,
|
||||
},
|
||||
// Disable Dockerode updates see https://github.com/apocas/dockerode/issues/787
|
||||
{
|
||||
matchPackagePatterns: ["^dockerode$"],
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
matchUpdateTypes: ["minor", "patch", "pin", "digest"],
|
||||
automerge: true,
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
token: ${{ github.token }}
|
||||
branch: dev
|
||||
- name: Create pull request
|
||||
run: 'gh pr create --title "chore(release): automatic release ${{ steps.semver.outputs.next }}" --body "**This is an automatic release**.<br/>Manual action may be required for major bumps.<br/>Detected change to be ``${{ steps.semver.outputs.bump }}``<br/>Bump version from ``${{ steps.semver.outputs.current }}`` to ``${{ steps.semver.outputs.next }}``" --base main --head dev --label automerge'
|
||||
run: 'gh pr create --title "chore(release): automatic release ${{ steps.semver.outputs.next }}" --body "**This is an automatic release**.<br/>Manual action may be required for major bumps.<br/>Detected change to be ``${{ steps.semver.outputs.bump }}``<br/>Bump version from ``${{ steps.semver.outputs.current }}`` to ``${{ steps.semver.outputs.next }}``" --base beta --head dev --label automerge'
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- name: Discord notification
|
||||
|
||||
+1
-1
@@ -64,4 +64,4 @@ packages/cli/cli.cjs
|
||||
e2e/shared/tmp
|
||||
|
||||
#personal backgrounds
|
||||
apps/nextjs/public/images/background.png
|
||||
apps/nextjs/public/images/background.png
|
||||
+1
-1167
File diff suppressed because it is too large.
Load diff
@@ -18,7 +18,6 @@
|
||||
"@homarr/analytics": "workspace:^0.1.0",
|
||||
"@homarr/api": "workspace:^0.1.0",
|
||||
"@homarr/auth": "workspace:^0.1.0",
|
||||
"@homarr/certificates": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/cron-job-status": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
@@ -62,7 +61,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^16.4.7",
|
||||
"flag-icons": "^7.3.1",
|
||||
"flag-icons": "^7.2.3",
|
||||
"glob": "^11.0.1",
|
||||
"jotai": "^2.11.0",
|
||||
"mantine-react-table": "2.0.0-beta.8",
|
||||
@@ -83,7 +82,7 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/chroma-js": "3.1.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/node": "^22.10.6",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "19.0.7",
|
||||
"@types/react-dom": "19.0.3",
|
||||
|
||||
+1
-3
@@ -1,4 +1,4 @@
|
||||
import { IconGrid3x3, IconKey, IconPassword, IconServer, IconUser } from "@tabler/icons-react";
|
||||
import { IconKey, IconPassword, IconUser } from "@tabler/icons-react";
|
||||
|
||||
import type { IntegrationSecretKind } from "@homarr/definitions";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
@@ -7,6 +7,4 @@ export const integrationSecretIcons = {
|
||||
username: IconUser,
|
||||
apiKey: IconKey,
|
||||
password: IconPassword,
|
||||
realm: IconServer,
|
||||
tokenId: IconGrid3x3,
|
||||
} satisfies Record<IntegrationSecretKind, TablerIcon>;
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
IconBrandDiscord,
|
||||
IconBrandDocker,
|
||||
IconBrandGithub,
|
||||
IconCertificate,
|
||||
IconGitFork,
|
||||
IconHome,
|
||||
IconInfoSmall,
|
||||
@@ -120,12 +119,6 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
|
||||
href: "/manage/tools/logs",
|
||||
hidden: !session?.user.permissions.includes("other-view-logs"),
|
||||
},
|
||||
{
|
||||
label: t("items.tools.items.certificates"),
|
||||
icon: IconCertificate,
|
||||
href: "/manage/tools/certificates",
|
||||
hidden: !session?.user.permissions.includes("admin"),
|
||||
},
|
||||
{
|
||||
label: t("items.tools.items.tasks"),
|
||||
icon: IconReport,
|
||||
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Button, FileInput, Group, Stack } from "@mantine/core";
|
||||
import { IconCertificate } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { createModal, useModalAction } from "@homarr/modals";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { superRefineCertificateFile, z } from "@homarr/validation";
|
||||
|
||||
export const AddCertificateButton = () => {
|
||||
const { openModal } = useModalAction(AddCertificateModal);
|
||||
const t = useI18n();
|
||||
|
||||
const handleClick = () => {
|
||||
openModal({});
|
||||
};
|
||||
|
||||
return <Button onClick={handleClick}>{t("certificate.action.create.label")}</Button>;
|
||||
};
|
||||
|
||||
const AddCertificateModal = createModal(({ actions }) => {
|
||||
const t = useI18n();
|
||||
const form = useZodForm(
|
||||
z.object({
|
||||
file: z.instanceof(File).nullable().superRefine(superRefineCertificateFile),
|
||||
}),
|
||||
{
|
||||
initialValues: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
file: null!,
|
||||
},
|
||||
},
|
||||
);
|
||||
const { mutateAsync } = clientApi.certificates.addCertificate.useMutation();
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.onSubmit(async (values) => {
|
||||
const formData = new FormData();
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
formData.set("file", values.file!);
|
||||
await mutateAsync(formData, {
|
||||
async onSuccess() {
|
||||
showSuccessNotification({
|
||||
title: t("certificate.action.create.notification.success.title"),
|
||||
message: t("certificate.action.create.notification.success.message"),
|
||||
});
|
||||
await revalidatePathActionAsync("/manage/tools/certificates");
|
||||
actions.closeModal();
|
||||
},
|
||||
onError() {
|
||||
showErrorNotification({
|
||||
title: t("certificate.action.create.notification.error.title"),
|
||||
message: t("certificate.action.create.notification.error.message"),
|
||||
});
|
||||
},
|
||||
});
|
||||
})}
|
||||
>
|
||||
<Stack>
|
||||
<FileInput leftSection={<IconCertificate size={16} />} {...form.getInputProps("file")} />
|
||||
<Group justify="end">
|
||||
<Button onClick={actions.closeModal} variant="subtle" color="gray">
|
||||
{t("common.action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" loading={form.submitting}>
|
||||
{t("common.action.add")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
}).withOptions({
|
||||
defaultTitle(t) {
|
||||
return t("certificate.action.create.label");
|
||||
},
|
||||
});
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ActionIcon } from "@mantine/core";
|
||||
import { IconTrash } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useConfirmModal } from "@homarr/modals";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
interface RemoveCertificateProps {
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export const RemoveCertificate = ({ fileName }: RemoveCertificateProps) => {
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const { mutateAsync } = clientApi.certificates.removeCertificate.useMutation();
|
||||
const t = useI18n();
|
||||
|
||||
const handleClick = () => {
|
||||
openConfirmModal({
|
||||
title: t("certificate.action.remove.label"),
|
||||
children: t("certificate.action.remove.confirm"),
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
async onConfirm() {
|
||||
await mutateAsync(
|
||||
{ fileName },
|
||||
{
|
||||
async onSuccess() {
|
||||
showSuccessNotification({
|
||||
title: t("certificate.action.remove.notification.success.title"),
|
||||
message: t("certificate.action.remove.notification.success.message"),
|
||||
});
|
||||
await revalidatePathActionAsync("/manage/tools/certificates");
|
||||
},
|
||||
onError() {
|
||||
showErrorNotification({
|
||||
title: t("certificate.action.remove.notification.error.title"),
|
||||
message: t("certificate.action.remove.notification.error.message"),
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionIcon onClick={handleClick} color="red" variant="subtle">
|
||||
<IconTrash color="red" size={16} />
|
||||
</ActionIcon>
|
||||
);
|
||||
};
|
||||
@@ -1,97 +0,0 @@
|
||||
import { X509Certificate } from "node:crypto";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Card, Group, SimpleGrid, Stack, Text, Title } from "@mantine/core";
|
||||
import { IconCertificate, IconCertificateOff } from "@tabler/icons-react";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { loadCustomRootCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { getMantineColor } from "@homarr/common";
|
||||
import type { SupportedLanguage } from "@homarr/translation";
|
||||
import { getI18n } from "@homarr/translation/server";
|
||||
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
import { NoResults } from "~/components/no-results";
|
||||
import { AddCertificateButton } from "./_components/add-certificate";
|
||||
import { RemoveCertificate } from "./_components/remove-certificate";
|
||||
|
||||
interface CertificatesPageProps {
|
||||
params: Promise<{
|
||||
locale: SupportedLanguage;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function CertificatesPage({ params }: CertificatesPageProps) {
|
||||
const session = await auth();
|
||||
if (!session?.user.permissions.includes("admin")) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { locale } = await params;
|
||||
const t = await getI18n();
|
||||
const certificates = await loadCustomRootCertificatesAsync();
|
||||
const x509Certificates = certificates
|
||||
.map((cert) => ({
|
||||
...cert,
|
||||
x509: new X509Certificate(cert.content),
|
||||
}))
|
||||
.sort((certA, certB) => certA.x509.validToDate.getTime() - certB.x509.validToDate.getTime());
|
||||
|
||||
return (
|
||||
<>
|
||||
<DynamicBreadcrumb />
|
||||
|
||||
<Stack>
|
||||
<Group justify="space-between">
|
||||
<Stack gap={4}>
|
||||
<Title>{t("certificate.page.list.title")}</Title>
|
||||
<Text>{t("certificate.page.list.description")}</Text>
|
||||
</Stack>
|
||||
|
||||
<AddCertificateButton />
|
||||
</Group>
|
||||
|
||||
{x509Certificates.length === 0 && (
|
||||
<NoResults icon={IconCertificateOff} title={t("certificate.page.list.noResults.title")} />
|
||||
)}
|
||||
|
||||
<SimpleGrid cols={{ sm: 1, lg: 2, xl: 3 }} spacing="lg">
|
||||
{x509Certificates.map((cert) => (
|
||||
<Card key={cert.x509.fingerprint} withBorder>
|
||||
<Group wrap="nowrap">
|
||||
<IconCertificate color={getMantineColor(iconColor(cert.x509.validToDate), 6)} size={32} stroke={1.5} />
|
||||
<Stack flex={1} gap="xs">
|
||||
<Group justify="space-between">
|
||||
<Text fw={500}>{cert.x509.subject}</Text>
|
||||
<Text c="gray.6" ta="end" size="sm">
|
||||
{cert.fileName}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c="gray.6" title={cert.x509.validToDate.toISOString()}>
|
||||
{t("certificate.page.list.expires", {
|
||||
when: new Intl.RelativeTimeFormat(locale).format(
|
||||
dayjs(cert.x509.validToDate).diff(dayjs(), "days"),
|
||||
"days",
|
||||
),
|
||||
})}
|
||||
</Text>
|
||||
<RemoveCertificate fileName={cert.fileName} />
|
||||
</Group>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const iconColor = (validTo: Date) => {
|
||||
const daysUntilInvalid = dayjs(validTo).diff(new Date(), "days");
|
||||
if (daysUntilInvalid < 1) return "red";
|
||||
if (daysUntilInvalid < 7) return "orange";
|
||||
if (daysUntilInvalid < 30) return "yellow";
|
||||
return "green";
|
||||
};
|
||||
@@ -38,13 +38,13 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^16.4.7",
|
||||
"superjson": "2.2.2",
|
||||
"undici": "7.2.3"
|
||||
"undici": "7.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/node": "^22.10.6",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"eslint": "^9.18.0",
|
||||
"prettier": "^3.4.2",
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { LoggingAgent } from "../fetch-agent";
|
||||
import { LoggingAgent } from "~/undici-log-agent-override";
|
||||
|
||||
vi.mock("undici", () => {
|
||||
return {
|
||||
@@ -1,6 +1,35 @@
|
||||
import { setGlobalDispatcher } from "undici";
|
||||
import type { Dispatcher } from "undici";
|
||||
import { Agent, setGlobalDispatcher } from "undici";
|
||||
|
||||
import { LoggingAgent } from "@homarr/common/server";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
export class LoggingAgent extends Agent {
|
||||
constructor(...props: ConstructorParameters<typeof Agent>) {
|
||||
super(...props);
|
||||
}
|
||||
|
||||
dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean {
|
||||
const url = new URL(`${options.origin as string}${options.path}`);
|
||||
|
||||
// The below code should prevent sensitive data from being logged as
|
||||
// some integrations use query parameters for auth
|
||||
url.searchParams.forEach((value, key) => {
|
||||
if (value === "") return; // Skip empty values
|
||||
if (/^-?\d{1,12}$/.test(value)) return; // Skip small numbers
|
||||
if (value === "true" || value === "false") return; // Skip boolean values
|
||||
if (/^[a-zA-Z]{1,12}$/.test(value)) return; // Skip short strings
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return; // Skip dates
|
||||
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)) return; // Skip date times
|
||||
|
||||
url.searchParams.set(key, "REDACTED");
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`Dispatching request ${url.toString().replaceAll("=&", "&")} (${Object.keys(options.headers ?? {}).length} headers)`,
|
||||
);
|
||||
return super.dispatch(options, handler);
|
||||
}
|
||||
}
|
||||
|
||||
const agent = new LoggingAgent();
|
||||
setGlobalDispatcher(agent);
|
||||
+1
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homarr",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.0-beta.13",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "cross-env CI=true turbo build",
|
||||
@@ -59,9 +59,6 @@
|
||||
},
|
||||
"pnpm": {
|
||||
"allowNonAppliedPatches": true,
|
||||
"overrides": {
|
||||
"proxmox-api>undici": "7.2.3"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"pretty-print-error": "patches/pretty-print-error.patch"
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/auth": "workspace:^0.1.0",
|
||||
"@homarr/certificates": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/cron-job-runner": "workspace:^0.1.0",
|
||||
"@homarr/cron-job-status": "workspace:^0.1.0",
|
||||
@@ -42,7 +41,7 @@
|
||||
"@trpc/client": "next",
|
||||
"@trpc/react-query": "next",
|
||||
"@trpc/server": "next",
|
||||
"dockerode": "4.0.2",
|
||||
"dockerode": "^4.0.3",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"next": "15.1.4",
|
||||
"react": "19.0.0",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { apiKeysRouter } from "./router/apiKeys";
|
||||
import { appRouter as innerAppRouter } from "./router/app";
|
||||
import { boardRouter } from "./router/board";
|
||||
import { certificateRouter } from "./router/certificates/certificate-router";
|
||||
import { cronJobsRouter } from "./router/cron-jobs";
|
||||
import { dockerRouter } from "./router/docker/docker-router";
|
||||
import { groupRouter } from "./router/group";
|
||||
@@ -42,7 +41,6 @@ export const appRouter = createTRPCRouter({
|
||||
apiKeys: apiKeysRouter,
|
||||
media: mediaRouter,
|
||||
updateChecker: updateCheckerRouter,
|
||||
certificates: certificateRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
+182
-449
@@ -4,7 +4,7 @@ import superjson from "superjson";
|
||||
import { constructBoardPermissions } from "@homarr/auth/shared";
|
||||
import type { DeviceType } from "@homarr/common/server";
|
||||
import type { Database, InferInsertModel, InferSelectModel, SQL } from "@homarr/db";
|
||||
import { and, createId, eq, handleTransactionsAsync, inArray, like, or } from "@homarr/db";
|
||||
import { and, createId, eq, inArray, like, or } from "@homarr/db";
|
||||
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||
import {
|
||||
boardGroupPermissions,
|
||||
@@ -204,49 +204,21 @@ export const boardRouter = createTRPCRouter({
|
||||
.input(validation.board.create)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const boardId = createId();
|
||||
await handleTransactionsAsync(ctx.db, {
|
||||
async handleAsync(db, schema) {
|
||||
await db.transaction(async (transaction) => {
|
||||
await transaction.insert(schema.boards).values({
|
||||
id: boardId,
|
||||
name: input.name,
|
||||
isPublic: input.isPublic,
|
||||
columnCount: input.columnCount,
|
||||
creatorId: ctx.session.user.id,
|
||||
});
|
||||
await transaction.insert(schema.sections).values({
|
||||
id: createId(),
|
||||
kind: "empty",
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
boardId,
|
||||
});
|
||||
});
|
||||
},
|
||||
handleSync(db) {
|
||||
db.transaction((transaction) => {
|
||||
transaction
|
||||
.insert(boards)
|
||||
.values({
|
||||
id: boardId,
|
||||
name: input.name,
|
||||
isPublic: input.isPublic,
|
||||
columnCount: input.columnCount,
|
||||
creatorId: ctx.session.user.id,
|
||||
})
|
||||
.run();
|
||||
transaction
|
||||
.insert(sections)
|
||||
.values({
|
||||
id: createId(),
|
||||
kind: "empty",
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
boardId,
|
||||
})
|
||||
.run();
|
||||
});
|
||||
},
|
||||
await ctx.db.transaction(async (transaction) => {
|
||||
await transaction.insert(boards).values({
|
||||
id: boardId,
|
||||
name: input.name,
|
||||
isPublic: input.isPublic,
|
||||
columnCount: input.columnCount,
|
||||
creatorId: ctx.session.user.id,
|
||||
});
|
||||
await transaction.insert(sections).values({
|
||||
id: createId(),
|
||||
kind: "empty",
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
boardId,
|
||||
});
|
||||
});
|
||||
}),
|
||||
duplicateBoard: permissionRequiredProcedure
|
||||
@@ -330,54 +302,28 @@ export const boardRouter = createTRPCRouter({
|
||||
})),
|
||||
);
|
||||
|
||||
await handleTransactionsAsync(ctx.db, {
|
||||
async handleAsync(db, schema) {
|
||||
await db.transaction(async (transaction) => {
|
||||
transaction.insert(schema.boards).values({
|
||||
...boardProps,
|
||||
id: newBoardId,
|
||||
name: input.name,
|
||||
creatorId: ctx.session.user.id,
|
||||
});
|
||||
ctx.db.transaction((transaction) => {
|
||||
transaction
|
||||
.insert(boards)
|
||||
.values({
|
||||
...boardProps,
|
||||
id: newBoardId,
|
||||
name: input.name,
|
||||
creatorId: ctx.session.user.id,
|
||||
})
|
||||
.run();
|
||||
|
||||
if (sectionsToInsert.length > 0) {
|
||||
await transaction.insert(schema.sections).values(sectionsToInsert);
|
||||
}
|
||||
if (sectionsToInsert.length > 0) {
|
||||
transaction.insert(sections).values(sectionsToInsert).run();
|
||||
}
|
||||
|
||||
if (itemsToInsert.length > 0) {
|
||||
await transaction.insert(schema.items).values(itemsToInsert);
|
||||
}
|
||||
if (itemsToInsert.length > 0) {
|
||||
transaction.insert(items).values(itemsToInsert).run();
|
||||
}
|
||||
|
||||
if (itemIntegrationsToInsert.length > 0) {
|
||||
await transaction.insert(schema.integrationItems).values(itemIntegrationsToInsert);
|
||||
}
|
||||
});
|
||||
},
|
||||
handleSync(db) {
|
||||
db.transaction((transaction) => {
|
||||
transaction
|
||||
.insert(boards)
|
||||
.values({
|
||||
...boardProps,
|
||||
id: newBoardId,
|
||||
name: input.name,
|
||||
creatorId: ctx.session.user.id,
|
||||
})
|
||||
.run();
|
||||
|
||||
if (sectionsToInsert.length > 0) {
|
||||
transaction.insert(sections).values(sectionsToInsert).run();
|
||||
}
|
||||
|
||||
if (itemsToInsert.length > 0) {
|
||||
transaction.insert(items).values(itemsToInsert).run();
|
||||
}
|
||||
|
||||
if (itemIntegrationsToInsert.length > 0) {
|
||||
transaction.insert(integrationItems).values(itemIntegrationsToInsert).run();
|
||||
}
|
||||
});
|
||||
},
|
||||
if (itemIntegrationsToInsert.length > 0) {
|
||||
transaction.insert(integrationItems).values(itemIntegrationsToInsert).run();
|
||||
}
|
||||
});
|
||||
}),
|
||||
renameBoard: protectedProcedure.input(validation.board.rename).mutation(async ({ ctx, input }) => {
|
||||
@@ -488,313 +434,148 @@ export const boardRouter = createTRPCRouter({
|
||||
saveBoard: protectedProcedure.input(validation.board.save).mutation(async ({ input, ctx }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
|
||||
|
||||
const dbBoard = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id);
|
||||
await ctx.db.transaction(async (transaction) => {
|
||||
const dbBoard = await getFullBoardWithWhereAsync(transaction, eq(boards.id, input.id), ctx.session.user.id);
|
||||
|
||||
await handleTransactionsAsync(ctx.db, {
|
||||
async handleAsync(db, schema) {
|
||||
await db.transaction(async (transaction) => {
|
||||
const addedSections = filterAddedItems(input.sections, dbBoard.sections);
|
||||
const addedSections = filterAddedItems(input.sections, dbBoard.sections);
|
||||
|
||||
if (addedSections.length > 0) {
|
||||
await transaction.insert(schema.sections).values(
|
||||
addedSections.map((section) => ({
|
||||
id: section.id,
|
||||
kind: section.kind,
|
||||
yOffset: section.yOffset,
|
||||
xOffset: section.kind === "dynamic" ? section.xOffset : 0,
|
||||
height: "height" in section ? section.height : null,
|
||||
width: "width" in section ? section.width : null,
|
||||
parentSectionId: "parentSectionId" in section ? section.parentSectionId : null,
|
||||
name: "name" in section ? section.name : null,
|
||||
boardId: dbBoard.id,
|
||||
})),
|
||||
);
|
||||
}
|
||||
if (addedSections.length > 0) {
|
||||
await transaction.insert(sections).values(
|
||||
addedSections.map((section) => ({
|
||||
id: section.id,
|
||||
kind: section.kind,
|
||||
yOffset: section.yOffset,
|
||||
xOffset: section.kind === "dynamic" ? section.xOffset : 0,
|
||||
height: "height" in section ? section.height : null,
|
||||
width: "width" in section ? section.width : null,
|
||||
parentSectionId: "parentSectionId" in section ? section.parentSectionId : null,
|
||||
name: "name" in section ? section.name : null,
|
||||
boardId: dbBoard.id,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
const inputItems = input.sections.flatMap((section) =>
|
||||
section.items.map((item) => ({ ...item, sectionId: section.id })),
|
||||
);
|
||||
const dbItems = dbBoard.sections.flatMap((section) =>
|
||||
section.items.map((item) => ({ ...item, sectionId: section.id })),
|
||||
);
|
||||
const inputItems = input.sections.flatMap((section) =>
|
||||
section.items.map((item) => ({ ...item, sectionId: section.id })),
|
||||
);
|
||||
const dbItems = dbBoard.sections.flatMap((section) =>
|
||||
section.items.map((item) => ({ ...item, sectionId: section.id })),
|
||||
);
|
||||
|
||||
const addedItems = filterAddedItems(inputItems, dbItems);
|
||||
const addedItems = filterAddedItems(inputItems, dbItems);
|
||||
|
||||
if (addedItems.length > 0) {
|
||||
await transaction.insert(schema.items).values(
|
||||
addedItems.map((item) => ({
|
||||
id: item.id,
|
||||
kind: item.kind,
|
||||
height: item.height,
|
||||
width: item.width,
|
||||
xOffset: item.xOffset,
|
||||
yOffset: item.yOffset,
|
||||
options: superjson.stringify(item.options),
|
||||
advancedOptions: superjson.stringify(item.advancedOptions),
|
||||
sectionId: item.sectionId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
if (addedItems.length > 0) {
|
||||
await transaction.insert(items).values(
|
||||
addedItems.map((item) => ({
|
||||
id: item.id,
|
||||
kind: item.kind,
|
||||
height: item.height,
|
||||
width: item.width,
|
||||
xOffset: item.xOffset,
|
||||
yOffset: item.yOffset,
|
||||
options: superjson.stringify(item.options),
|
||||
advancedOptions: superjson.stringify(item.advancedOptions),
|
||||
sectionId: item.sectionId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
const inputIntegrationRelations = inputItems.flatMap(({ integrationIds, id: itemId }) =>
|
||||
integrationIds.map((integrationId) => ({
|
||||
integrationId,
|
||||
itemId,
|
||||
})),
|
||||
);
|
||||
const dbIntegrationRelations = dbItems.flatMap(({ integrationIds, id: itemId }) =>
|
||||
integrationIds.map((integrationId) => ({
|
||||
integrationId,
|
||||
itemId,
|
||||
})),
|
||||
);
|
||||
const addedIntegrationRelations = inputIntegrationRelations.filter(
|
||||
(inputRelation) =>
|
||||
!dbIntegrationRelations.some(
|
||||
(dbRelation) =>
|
||||
dbRelation.itemId === inputRelation.itemId &&
|
||||
dbRelation.integrationId === inputRelation.integrationId,
|
||||
),
|
||||
);
|
||||
|
||||
if (addedIntegrationRelations.length > 0) {
|
||||
await transaction.insert(schema.integrationItems).values(
|
||||
addedIntegrationRelations.map((relation) => ({
|
||||
itemId: relation.itemId,
|
||||
integrationId: relation.integrationId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
const updatedItems = filterUpdatedItems(inputItems, dbItems);
|
||||
|
||||
for (const item of updatedItems) {
|
||||
await transaction
|
||||
.update(schema.items)
|
||||
.set({
|
||||
kind: item.kind,
|
||||
height: item.height,
|
||||
width: item.width,
|
||||
xOffset: item.xOffset,
|
||||
yOffset: item.yOffset,
|
||||
options: superjson.stringify(item.options),
|
||||
advancedOptions: superjson.stringify(item.advancedOptions),
|
||||
sectionId: item.sectionId,
|
||||
})
|
||||
.where(eq(items.id, item.id));
|
||||
}
|
||||
|
||||
const updatedSections = filterUpdatedItems(input.sections, dbBoard.sections);
|
||||
|
||||
for (const section of updatedSections) {
|
||||
const prev = dbBoard.sections.find((dbSection) => dbSection.id === section.id);
|
||||
await transaction
|
||||
.update(schema.sections)
|
||||
.set({
|
||||
yOffset: section.yOffset,
|
||||
xOffset: section.xOffset,
|
||||
height: prev?.kind === "dynamic" && "height" in section ? section.height : null,
|
||||
width: prev?.kind === "dynamic" && "width" in section ? section.width : null,
|
||||
parentSectionId:
|
||||
prev?.kind === "dynamic" && "parentSectionId" in section ? section.parentSectionId : null,
|
||||
name: prev?.kind === "category" && "name" in section ? section.name : null,
|
||||
})
|
||||
.where(eq(sections.id, section.id));
|
||||
}
|
||||
|
||||
const removedIntegrationRelations = dbIntegrationRelations.filter(
|
||||
const inputIntegrationRelations = inputItems.flatMap(({ integrationIds, id: itemId }) =>
|
||||
integrationIds.map((integrationId) => ({
|
||||
integrationId,
|
||||
itemId,
|
||||
})),
|
||||
);
|
||||
const dbIntegrationRelations = dbItems.flatMap(({ integrationIds, id: itemId }) =>
|
||||
integrationIds.map((integrationId) => ({
|
||||
integrationId,
|
||||
itemId,
|
||||
})),
|
||||
);
|
||||
const addedIntegrationRelations = inputIntegrationRelations.filter(
|
||||
(inputRelation) =>
|
||||
!dbIntegrationRelations.some(
|
||||
(dbRelation) =>
|
||||
!inputIntegrationRelations.some(
|
||||
(inputRelation) =>
|
||||
dbRelation.itemId === inputRelation.itemId &&
|
||||
dbRelation.integrationId === inputRelation.integrationId,
|
||||
),
|
||||
);
|
||||
dbRelation.itemId === inputRelation.itemId && dbRelation.integrationId === inputRelation.integrationId,
|
||||
),
|
||||
);
|
||||
|
||||
for (const relation of removedIntegrationRelations) {
|
||||
await transaction
|
||||
.delete(schema.integrationItems)
|
||||
.where(
|
||||
and(
|
||||
eq(integrationItems.itemId, relation.itemId),
|
||||
eq(integrationItems.integrationId, relation.integrationId),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (addedIntegrationRelations.length > 0) {
|
||||
await transaction.insert(integrationItems).values(
|
||||
addedIntegrationRelations.map((relation) => ({
|
||||
itemId: relation.itemId,
|
||||
integrationId: relation.integrationId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
const removedItems = filterRemovedItems(inputItems, dbItems);
|
||||
const updatedItems = filterUpdatedItems(inputItems, dbItems);
|
||||
|
||||
const itemIds = removedItems.map((item) => item.id);
|
||||
if (itemIds.length > 0) {
|
||||
await transaction.delete(schema.items).where(inArray(items.id, itemIds));
|
||||
}
|
||||
for (const item of updatedItems) {
|
||||
await transaction
|
||||
.update(items)
|
||||
.set({
|
||||
kind: item.kind,
|
||||
height: item.height,
|
||||
width: item.width,
|
||||
xOffset: item.xOffset,
|
||||
yOffset: item.yOffset,
|
||||
options: superjson.stringify(item.options),
|
||||
advancedOptions: superjson.stringify(item.advancedOptions),
|
||||
sectionId: item.sectionId,
|
||||
})
|
||||
.where(eq(items.id, item.id));
|
||||
}
|
||||
|
||||
const removedSections = filterRemovedItems(input.sections, dbBoard.sections);
|
||||
const sectionIds = removedSections.map((section) => section.id);
|
||||
const updatedSections = filterUpdatedItems(input.sections, dbBoard.sections);
|
||||
|
||||
if (sectionIds.length > 0) {
|
||||
await transaction.delete(schema.sections).where(inArray(sections.id, sectionIds));
|
||||
}
|
||||
});
|
||||
},
|
||||
handleSync(db) {
|
||||
db.transaction((transaction) => {
|
||||
const addedSections = filterAddedItems(input.sections, dbBoard.sections);
|
||||
for (const section of updatedSections) {
|
||||
const prev = dbBoard.sections.find((dbSection) => dbSection.id === section.id);
|
||||
await transaction
|
||||
.update(sections)
|
||||
.set({
|
||||
yOffset: section.yOffset,
|
||||
xOffset: section.xOffset,
|
||||
height: prev?.kind === "dynamic" && "height" in section ? section.height : null,
|
||||
width: prev?.kind === "dynamic" && "width" in section ? section.width : null,
|
||||
parentSectionId: prev?.kind === "dynamic" && "parentSectionId" in section ? section.parentSectionId : null,
|
||||
name: prev?.kind === "category" && "name" in section ? section.name : null,
|
||||
})
|
||||
.where(eq(sections.id, section.id));
|
||||
}
|
||||
|
||||
if (addedSections.length > 0) {
|
||||
transaction
|
||||
.insert(sections)
|
||||
.values(
|
||||
addedSections.map((section) => ({
|
||||
id: section.id,
|
||||
kind: section.kind,
|
||||
yOffset: section.yOffset,
|
||||
xOffset: section.kind === "dynamic" ? section.xOffset : 0,
|
||||
height: "height" in section ? section.height : null,
|
||||
width: "width" in section ? section.width : null,
|
||||
parentSectionId: "parentSectionId" in section ? section.parentSectionId : null,
|
||||
name: "name" in section ? section.name : null,
|
||||
boardId: dbBoard.id,
|
||||
})),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
const inputItems = input.sections.flatMap((section) =>
|
||||
section.items.map((item) => ({ ...item, sectionId: section.id })),
|
||||
);
|
||||
const dbItems = dbBoard.sections.flatMap((section) =>
|
||||
section.items.map((item) => ({ ...item, sectionId: section.id })),
|
||||
);
|
||||
|
||||
const addedItems = filterAddedItems(inputItems, dbItems);
|
||||
|
||||
if (addedItems.length > 0) {
|
||||
transaction
|
||||
.insert(items)
|
||||
.values(
|
||||
addedItems.map((item) => ({
|
||||
id: item.id,
|
||||
kind: item.kind,
|
||||
height: item.height,
|
||||
width: item.width,
|
||||
xOffset: item.xOffset,
|
||||
yOffset: item.yOffset,
|
||||
options: superjson.stringify(item.options),
|
||||
advancedOptions: superjson.stringify(item.advancedOptions),
|
||||
sectionId: item.sectionId,
|
||||
})),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
const inputIntegrationRelations = inputItems.flatMap(({ integrationIds, id: itemId }) =>
|
||||
integrationIds.map((integrationId) => ({
|
||||
integrationId,
|
||||
itemId,
|
||||
})),
|
||||
);
|
||||
const dbIntegrationRelations = dbItems.flatMap(({ integrationIds, id: itemId }) =>
|
||||
integrationIds.map((integrationId) => ({
|
||||
integrationId,
|
||||
itemId,
|
||||
})),
|
||||
);
|
||||
const addedIntegrationRelations = inputIntegrationRelations.filter(
|
||||
const removedIntegrationRelations = dbIntegrationRelations.filter(
|
||||
(dbRelation) =>
|
||||
!inputIntegrationRelations.some(
|
||||
(inputRelation) =>
|
||||
!dbIntegrationRelations.some(
|
||||
(dbRelation) =>
|
||||
dbRelation.itemId === inputRelation.itemId &&
|
||||
dbRelation.integrationId === inputRelation.integrationId,
|
||||
),
|
||||
dbRelation.itemId === inputRelation.itemId && dbRelation.integrationId === inputRelation.integrationId,
|
||||
),
|
||||
);
|
||||
|
||||
for (const relation of removedIntegrationRelations) {
|
||||
await transaction
|
||||
.delete(integrationItems)
|
||||
.where(
|
||||
and(
|
||||
eq(integrationItems.itemId, relation.itemId),
|
||||
eq(integrationItems.integrationId, relation.integrationId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (addedIntegrationRelations.length > 0) {
|
||||
transaction
|
||||
.insert(integrationItems)
|
||||
.values(
|
||||
addedIntegrationRelations.map((relation) => ({
|
||||
itemId: relation.itemId,
|
||||
integrationId: relation.integrationId,
|
||||
})),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
const removedItems = filterRemovedItems(inputItems, dbItems);
|
||||
|
||||
const updatedItems = filterUpdatedItems(inputItems, dbItems);
|
||||
const itemIds = removedItems.map((item) => item.id);
|
||||
if (itemIds.length > 0) {
|
||||
await transaction.delete(items).where(inArray(items.id, itemIds));
|
||||
}
|
||||
|
||||
for (const item of updatedItems) {
|
||||
transaction
|
||||
.update(items)
|
||||
.set({
|
||||
kind: item.kind,
|
||||
height: item.height,
|
||||
width: item.width,
|
||||
xOffset: item.xOffset,
|
||||
yOffset: item.yOffset,
|
||||
options: superjson.stringify(item.options),
|
||||
advancedOptions: superjson.stringify(item.advancedOptions),
|
||||
sectionId: item.sectionId,
|
||||
})
|
||||
.where(eq(items.id, item.id))
|
||||
.run();
|
||||
}
|
||||
const removedSections = filterRemovedItems(input.sections, dbBoard.sections);
|
||||
const sectionIds = removedSections.map((section) => section.id);
|
||||
|
||||
const updatedSections = filterUpdatedItems(input.sections, dbBoard.sections);
|
||||
|
||||
for (const section of updatedSections) {
|
||||
const prev = dbBoard.sections.find((dbSection) => dbSection.id === section.id);
|
||||
transaction
|
||||
.update(sections)
|
||||
.set({
|
||||
yOffset: section.yOffset,
|
||||
xOffset: section.xOffset,
|
||||
height: prev?.kind === "dynamic" && "height" in section ? section.height : null,
|
||||
width: prev?.kind === "dynamic" && "width" in section ? section.width : null,
|
||||
parentSectionId:
|
||||
prev?.kind === "dynamic" && "parentSectionId" in section ? section.parentSectionId : null,
|
||||
name: prev?.kind === "category" && "name" in section ? section.name : null,
|
||||
})
|
||||
.where(eq(sections.id, section.id))
|
||||
.run();
|
||||
}
|
||||
|
||||
const removedIntegrationRelations = dbIntegrationRelations.filter(
|
||||
(dbRelation) =>
|
||||
!inputIntegrationRelations.some(
|
||||
(inputRelation) =>
|
||||
dbRelation.itemId === inputRelation.itemId &&
|
||||
dbRelation.integrationId === inputRelation.integrationId,
|
||||
),
|
||||
);
|
||||
|
||||
for (const relation of removedIntegrationRelations) {
|
||||
transaction
|
||||
.delete(integrationItems)
|
||||
.where(
|
||||
and(
|
||||
eq(integrationItems.itemId, relation.itemId),
|
||||
eq(integrationItems.integrationId, relation.integrationId),
|
||||
),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
const removedItems = filterRemovedItems(inputItems, dbItems);
|
||||
|
||||
const itemIds = removedItems.map((item) => item.id);
|
||||
if (itemIds.length > 0) {
|
||||
transaction.delete(items).where(inArray(items.id, itemIds)).run();
|
||||
}
|
||||
|
||||
const removedSections = filterRemovedItems(input.sections, dbBoard.sections);
|
||||
const sectionIds = removedSections.map((section) => section.id);
|
||||
|
||||
if (sectionIds.length > 0) {
|
||||
transaction.delete(sections).where(inArray(sections.id, sectionIds)).run();
|
||||
}
|
||||
});
|
||||
},
|
||||
if (sectionIds.length > 0) {
|
||||
await transaction.delete(sections).where(inArray(sections.id, sectionIds));
|
||||
}
|
||||
});
|
||||
}),
|
||||
|
||||
@@ -874,42 +655,18 @@ export const boardRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full");
|
||||
|
||||
await handleTransactionsAsync(ctx.db, {
|
||||
async handleAsync(db, schema) {
|
||||
await db.transaction(async (transaction) => {
|
||||
await transaction
|
||||
.delete(schema.boardUserPermissions)
|
||||
.where(eq(boardUserPermissions.boardId, input.entityId));
|
||||
if (input.permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
await transaction.insert(schema.boardUserPermissions).values(
|
||||
input.permissions.map((permission) => ({
|
||||
userId: permission.principalId,
|
||||
permission: permission.permission,
|
||||
boardId: input.entityId,
|
||||
})),
|
||||
);
|
||||
});
|
||||
},
|
||||
handleSync(db) {
|
||||
db.transaction((transaction) => {
|
||||
transaction.delete(boardUserPermissions).where(eq(boardUserPermissions.boardId, input.entityId)).run();
|
||||
if (input.permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
transaction
|
||||
.insert(boardUserPermissions)
|
||||
.values(
|
||||
input.permissions.map((permission) => ({
|
||||
userId: permission.principalId,
|
||||
permission: permission.permission,
|
||||
boardId: input.entityId,
|
||||
})),
|
||||
)
|
||||
.run();
|
||||
});
|
||||
},
|
||||
await ctx.db.transaction(async (transaction) => {
|
||||
await transaction.delete(boardUserPermissions).where(eq(boardUserPermissions.boardId, input.entityId));
|
||||
if (input.permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
await transaction.insert(boardUserPermissions).values(
|
||||
input.permissions.map((permission) => ({
|
||||
userId: permission.principalId,
|
||||
permission: permission.permission,
|
||||
boardId: input.entityId,
|
||||
})),
|
||||
);
|
||||
});
|
||||
}),
|
||||
saveGroupBoardPermissions: protectedProcedure
|
||||
@@ -917,42 +674,18 @@ export const boardRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full");
|
||||
|
||||
await handleTransactionsAsync(ctx.db, {
|
||||
async handleAsync(db, schema) {
|
||||
await db.transaction(async (transaction) => {
|
||||
await transaction
|
||||
.delete(schema.boardGroupPermissions)
|
||||
.where(eq(boardGroupPermissions.boardId, input.entityId));
|
||||
if (input.permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
await transaction.insert(schema.boardGroupPermissions).values(
|
||||
input.permissions.map((permission) => ({
|
||||
groupId: permission.principalId,
|
||||
permission: permission.permission,
|
||||
boardId: input.entityId,
|
||||
})),
|
||||
);
|
||||
});
|
||||
},
|
||||
handleSync(db) {
|
||||
db.transaction((transaction) => {
|
||||
transaction.delete(boardGroupPermissions).where(eq(boardGroupPermissions.boardId, input.entityId)).run();
|
||||
if (input.permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
transaction
|
||||
.insert(boardGroupPermissions)
|
||||
.values(
|
||||
input.permissions.map((permission) => ({
|
||||
groupId: permission.principalId,
|
||||
permission: permission.permission,
|
||||
boardId: input.entityId,
|
||||
})),
|
||||
)
|
||||
.run();
|
||||
});
|
||||
},
|
||||
await ctx.db.transaction(async (transaction) => {
|
||||
await transaction.delete(boardGroupPermissions).where(eq(boardGroupPermissions.boardId, input.entityId));
|
||||
if (input.permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
await transaction.insert(boardGroupPermissions).values(
|
||||
input.permissions.map((permission) => ({
|
||||
groupId: permission.principalId,
|
||||
permission: permission.permission,
|
||||
boardId: input.entityId,
|
||||
})),
|
||||
);
|
||||
});
|
||||
}),
|
||||
importOldmarrConfig: permissionRequiredProcedure
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { zfd } from "zod-form-data";
|
||||
|
||||
import { addCustomRootCertificateAsync, removeCustomRootCertificateAsync } from "@homarr/certificates/server";
|
||||
import { superRefineCertificateFile, validation } from "@homarr/validation";
|
||||
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
|
||||
|
||||
export const certificateRouter = createTRPCRouter({
|
||||
addCertificate: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(
|
||||
zfd.formData({
|
||||
file: zfd.file().superRefine(superRefineCertificateFile),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const content = await input.file.text();
|
||||
await addCustomRootCertificateAsync(input.file.name, content);
|
||||
}),
|
||||
removeCertificate: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(z.object({ fileName: validation.certificates.validFileNameSchema }))
|
||||
.mutation(async ({ input }) => {
|
||||
await removeCustomRootCertificateAsync(input.fileName);
|
||||
}),
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { TRPCError } from "@trpc/server";
|
||||
import { objectEntries } from "@homarr/common";
|
||||
import { decryptSecret, encryptSecret } from "@homarr/common/server";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { and, asc, createId, eq, handleTransactionsAsync, inArray, like } from "@homarr/db";
|
||||
import { and, asc, createId, eq, inArray, like } from "@homarr/db";
|
||||
import {
|
||||
groupMembers,
|
||||
groupPermissions,
|
||||
@@ -130,16 +130,6 @@ export const integrationRouter = createTRPCRouter({
|
||||
limit: input.limit,
|
||||
});
|
||||
}),
|
||||
// This is used to get the integrations by their ids it's public because it's needed to get integrations data in the boards
|
||||
byIds: publicProcedure.input(z.array(z.string())).query(async ({ ctx, input }) => {
|
||||
return await ctx.db.query.integrations.findMany({
|
||||
where: inArray(integrations.id, input),
|
||||
columns: {
|
||||
id: true,
|
||||
kind: true,
|
||||
},
|
||||
});
|
||||
}),
|
||||
byId: protectedProcedure.input(validation.integration.byId).query(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
|
||||
const integration = await ctx.db.query.integrations.findFirst({
|
||||
@@ -370,45 +360,20 @@ export const integrationRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.entityId), "full");
|
||||
|
||||
await handleTransactionsAsync(ctx.db, {
|
||||
async handleAsync(db, schema) {
|
||||
await ctx.db.transaction(async (transaction) => {
|
||||
await transaction
|
||||
.delete(schema.integrationUserPermissions)
|
||||
.where(eq(schema.integrationUserPermissions.integrationId, input.entityId));
|
||||
if (input.permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
await transaction.insert(schema.integrationUserPermissions).values(
|
||||
input.permissions.map((permission) => ({
|
||||
userId: permission.principalId,
|
||||
permission: permission.permission,
|
||||
integrationId: input.entityId,
|
||||
})),
|
||||
);
|
||||
});
|
||||
},
|
||||
handleSync(db) {
|
||||
db.transaction((transaction) => {
|
||||
transaction
|
||||
.delete(integrationUserPermissions)
|
||||
.where(eq(integrationUserPermissions.integrationId, input.entityId))
|
||||
.run();
|
||||
if (input.permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
transaction
|
||||
.insert(integrationUserPermissions)
|
||||
.values(
|
||||
input.permissions.map((permission) => ({
|
||||
userId: permission.principalId,
|
||||
permission: permission.permission,
|
||||
integrationId: input.entityId,
|
||||
})),
|
||||
)
|
||||
.run();
|
||||
});
|
||||
},
|
||||
await ctx.db.transaction(async (transaction) => {
|
||||
await transaction
|
||||
.delete(integrationUserPermissions)
|
||||
.where(eq(integrationUserPermissions.integrationId, input.entityId));
|
||||
if (input.permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
await transaction.insert(integrationUserPermissions).values(
|
||||
input.permissions.map((permission) => ({
|
||||
userId: permission.principalId,
|
||||
permission: permission.permission,
|
||||
integrationId: input.entityId,
|
||||
})),
|
||||
);
|
||||
});
|
||||
}),
|
||||
saveGroupIntegrationPermissions: protectedProcedure
|
||||
@@ -416,45 +381,20 @@ export const integrationRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.entityId), "full");
|
||||
|
||||
await handleTransactionsAsync(ctx.db, {
|
||||
async handleAsync(db, schema) {
|
||||
await db.transaction(async (transaction) => {
|
||||
await transaction
|
||||
.delete(schema.integrationGroupPermissions)
|
||||
.where(eq(schema.integrationGroupPermissions.integrationId, input.entityId));
|
||||
if (input.permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
await transaction.insert(schema.integrationGroupPermissions).values(
|
||||
input.permissions.map((permission) => ({
|
||||
groupId: permission.principalId,
|
||||
permission: permission.permission,
|
||||
integrationId: input.entityId,
|
||||
})),
|
||||
);
|
||||
});
|
||||
},
|
||||
handleSync(db) {
|
||||
db.transaction((transaction) => {
|
||||
transaction
|
||||
.delete(integrationGroupPermissions)
|
||||
.where(eq(integrationGroupPermissions.integrationId, input.entityId))
|
||||
.run();
|
||||
if (input.permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
transaction
|
||||
.insert(integrationGroupPermissions)
|
||||
.values(
|
||||
input.permissions.map((permission) => ({
|
||||
groupId: permission.principalId,
|
||||
permission: permission.permission,
|
||||
integrationId: input.entityId,
|
||||
})),
|
||||
)
|
||||
.run();
|
||||
});
|
||||
},
|
||||
await ctx.db.transaction(async (transaction) => {
|
||||
await transaction
|
||||
.delete(integrationGroupPermissions)
|
||||
.where(eq(integrationGroupPermissions.integrationId, input.entityId));
|
||||
if (input.permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
await transaction.insert(integrationGroupPermissions).values(
|
||||
input.permissions.map((permission) => ({
|
||||
groupId: permission.principalId,
|
||||
permission: permission.permission,
|
||||
integrationId: input.entityId,
|
||||
})),
|
||||
);
|
||||
});
|
||||
}),
|
||||
searchInIntegration: protectedProcedure
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { observable } from "@trpc/server/observable";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import type { HealthMonitoring } from "@homarr/integrations";
|
||||
import type { ProxmoxClusterInfo } from "@homarr/integrations/types";
|
||||
import { clusterInfoRequestHandler, systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring";
|
||||
import { systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring";
|
||||
|
||||
import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const healthMonitoringRouter = createTRPCRouter({
|
||||
getSystemHealthStatus: publicProcedure
|
||||
.unstable_concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot"))
|
||||
getHealthStatus: publicProcedure
|
||||
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("healthMonitoring")))
|
||||
.query(async ({ ctx }) => {
|
||||
return await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
@@ -25,8 +25,9 @@ export const healthMonitoringRouter = createTRPCRouter({
|
||||
}),
|
||||
);
|
||||
}),
|
||||
subscribeSystemHealthStatus: publicProcedure
|
||||
.unstable_concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot"))
|
||||
|
||||
subscribeHealthStatus: publicProcedure
|
||||
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("healthMonitoring")))
|
||||
.subscription(({ ctx }) => {
|
||||
return observable<{ integrationId: string; healthInfo: HealthMonitoring; timestamp: Date }>((emit) => {
|
||||
const unsubscribes: (() => void)[] = [];
|
||||
@@ -48,26 +49,4 @@ export const healthMonitoringRouter = createTRPCRouter({
|
||||
};
|
||||
});
|
||||
}),
|
||||
getClusterHealthStatus: publicProcedure
|
||||
.unstable_concat(createOneIntegrationMiddleware("query", "proxmox"))
|
||||
.query(async ({ ctx }) => {
|
||||
const innerHandler = clusterInfoRequestHandler.handler(ctx.integration, {});
|
||||
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||
return data;
|
||||
}),
|
||||
subscribeClusterHealthStatus: publicProcedure
|
||||
.unstable_concat(createOneIntegrationMiddleware("query", "proxmox"))
|
||||
.subscription(({ ctx }) => {
|
||||
return observable<ProxmoxClusterInfo>((emit) => {
|
||||
const unsubscribes: (() => void)[] = [];
|
||||
const innerHandler = clusterInfoRequestHandler.handler(ctx.integration, {});
|
||||
const unsubscribe = innerHandler.subscribe((healthInfo) => {
|
||||
emit.next(healthInfo);
|
||||
});
|
||||
unsubscribes.push(unsubscribe);
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
});
|
||||
}),
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import baseConfig from "@homarr/eslint-config/base";
|
||||
|
||||
/** @type {import('typescript-eslint').Config} */
|
||||
export default [
|
||||
{
|
||||
ignores: [],
|
||||
},
|
||||
...baseConfig,
|
||||
];
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "@homarr/certificates",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./server": "./src/server.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf .turbo node_modules",
|
||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||
"lint": "eslint",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"undici": "7.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.18.0",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { Agent } from "node:https";
|
||||
import path from "node:path";
|
||||
import { rootCertificates } from "node:tls";
|
||||
import axios from "axios";
|
||||
import { fetch } from "undici";
|
||||
|
||||
import { LoggingAgent } from "@homarr/common/server";
|
||||
|
||||
const getCertificateFolder = () => {
|
||||
return process.env.NODE_ENV === "production"
|
||||
? path.join("/appdata", "trusted-certificates")
|
||||
: process.env.LOCAL_CERTIFICATE_PATH;
|
||||
};
|
||||
|
||||
export const loadCustomRootCertificatesAsync = async () => {
|
||||
const folder = getCertificateFolder();
|
||||
|
||||
if (!folder) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!fsSync.existsSync(folder)) {
|
||||
await fs.mkdir(folder, { recursive: true });
|
||||
}
|
||||
|
||||
const dirContent = await fs.readdir(folder);
|
||||
return await Promise.all(
|
||||
dirContent
|
||||
.filter((file) => file.endsWith(".crt"))
|
||||
.map(async (file) => ({
|
||||
content: await fs.readFile(path.join(folder, file), "utf8"),
|
||||
fileName: file,
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
export const removeCustomRootCertificateAsync = async (fileName: string) => {
|
||||
const folder = getCertificateFolder();
|
||||
if (!folder) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.rm(path.join(folder, fileName));
|
||||
};
|
||||
|
||||
export const addCustomRootCertificateAsync = async (fileName: string, content: string) => {
|
||||
const folder = getCertificateFolder();
|
||||
if (!folder) {
|
||||
throw new Error(
|
||||
"When you want to use custom certificates locally you need to set LOCAL_CERTIFICATE_PATH to an absolute path",
|
||||
);
|
||||
}
|
||||
|
||||
if (fileName.includes("/")) {
|
||||
throw new Error("Invalid file name");
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(folder, fileName), content);
|
||||
};
|
||||
|
||||
export const createCertificateAgentAsync = async () => {
|
||||
const customCertificates = await loadCustomRootCertificatesAsync();
|
||||
return new LoggingAgent({
|
||||
connect: {
|
||||
ca: rootCertificates.concat(customCertificates.map((cert) => cert.content)),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const createAxiosCertificateInstanceAsync = async () => {
|
||||
const customCertificates = await loadCustomRootCertificatesAsync();
|
||||
return axios.create({
|
||||
httpsAgent: new Agent({
|
||||
ca: rootCertificates.concat(customCertificates.map((cert) => cert.content)),
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchWithTrustedCertificatesAsync: typeof fetch = async (url, options) => {
|
||||
const agent = await createCertificateAgentAsync();
|
||||
return fetch(url, {
|
||||
...options,
|
||||
dispatcher: agent,
|
||||
});
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "@homarr/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
"include": ["*.ts", "src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -32,7 +32,6 @@
|
||||
"next": "15.1.4",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"undici": "7.2.3",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import type { Dispatcher } from "undici";
|
||||
import { Agent } from "undici";
|
||||
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
export class LoggingAgent extends Agent {
|
||||
constructor(...props: ConstructorParameters<typeof Agent>) {
|
||||
super(...props);
|
||||
}
|
||||
|
||||
dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean {
|
||||
const url = new URL(`${options.origin as string}${options.path}`);
|
||||
|
||||
// The below code should prevent sensitive data from being logged as
|
||||
// some integrations use query parameters for auth
|
||||
url.searchParams.forEach((value, key) => {
|
||||
if (value === "") return; // Skip empty values
|
||||
if (/^-?\d{1,12}$/.test(value)) return; // Skip small numbers
|
||||
if (value === "true" || value === "false") return; // Skip boolean values
|
||||
if (/^[a-zA-Z]{1,12}$/.test(value)) return; // Skip short strings
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return; // Skip dates
|
||||
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)) return; // Skip date times
|
||||
|
||||
url.searchParams.set(key, "REDACTED");
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`Dispatching request ${url.toString().replaceAll("=&", "&")} (${Object.keys(options.headers ?? {}).length} headers)`,
|
||||
);
|
||||
return super.dispatch(options, handler);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from "./security";
|
||||
export * from "./encryption";
|
||||
export * from "./user-agent";
|
||||
export * from "./fetch-agent";
|
||||
@@ -1,7 +1,7 @@
|
||||
import { splitToNChunks, Stopwatch } from "@homarr/common";
|
||||
import { EVERY_WEEK } from "@homarr/cron-jobs-core/expressions";
|
||||
import type { InferInsertModel } from "@homarr/db";
|
||||
import { db, handleTransactionsAsync, inArray, sql } from "@homarr/db";
|
||||
import { db, inArray, sql } from "@homarr/db";
|
||||
import { createId } from "@homarr/db/client";
|
||||
import { iconRepositories, icons } from "@homarr/db/schema";
|
||||
import { fetchIconsAsync } from "@homarr/icons";
|
||||
@@ -83,81 +83,43 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
|
||||
(iconRepository) => !repositoryIconGroups.some((group) => group.slug === iconRepository.slug),
|
||||
);
|
||||
|
||||
await handleTransactionsAsync(db, {
|
||||
async handleAsync(db, schema) {
|
||||
await db.transaction(async (transaction) => {
|
||||
if (newIconRepositories.length >= 1) {
|
||||
await transaction.insert(schema.iconRepositories).values(newIconRepositories);
|
||||
}
|
||||
db.transaction((transaction) => {
|
||||
if (newIconRepositories.length >= 1) {
|
||||
transaction.insert(iconRepositories).values(newIconRepositories).run();
|
||||
}
|
||||
|
||||
if (newIcons.length >= 1) {
|
||||
// We only insert 5000 icons at a time to avoid SQLite limitations
|
||||
for (const chunck of splitToNChunks(newIcons, Math.ceil(newIcons.length / 5000))) {
|
||||
await transaction.insert(schema.icons).values(chunck);
|
||||
}
|
||||
}
|
||||
if (deadIcons.length >= 1) {
|
||||
await transaction.delete(schema.icons).where(
|
||||
inArray(
|
||||
// Combine iconRepositoryId and checksum to allow same icons on different repositories
|
||||
sql`concat(${icons.iconRepositoryId}, '.', ${icons.checksum})`,
|
||||
deadIcons.map((icon) => `${icon.iconRepositoryId}.${icon.checksum}`),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (newIcons.length >= 1) {
|
||||
// We only insert 5000 icons at a time to avoid SQLite limitations
|
||||
for (const chunck of splitToNChunks(newIcons, Math.ceil(newIcons.length / 5000))) {
|
||||
transaction.insert(icons).values(chunck).run();
|
||||
}
|
||||
}
|
||||
if (deadIcons.length >= 1) {
|
||||
transaction
|
||||
.delete(icons)
|
||||
.where(
|
||||
inArray(
|
||||
// Combine iconRepositoryId and checksum to allow same icons on different repositories
|
||||
sql`concat(${icons.iconRepositoryId}, '.', ${icons.checksum})`,
|
||||
deadIcons.map((icon) => `${icon.iconRepositoryId}.${icon.checksum}`),
|
||||
),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
if (deadIconRepositories.length >= 1) {
|
||||
await transaction.delete(schema.iconRepositories).where(
|
||||
inArray(
|
||||
iconRepositories.id,
|
||||
deadIconRepositories.map((iconRepository) => iconRepository.id),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (deadIconRepositories.length >= 1) {
|
||||
transaction
|
||||
.delete(iconRepositories)
|
||||
.where(
|
||||
inArray(
|
||||
iconRepositories.id,
|
||||
deadIconRepositories.map((iconRepository) => iconRepository.id),
|
||||
),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
countDeleted += deadIcons.length;
|
||||
});
|
||||
},
|
||||
handleSync() {
|
||||
db.transaction((transaction) => {
|
||||
if (newIconRepositories.length >= 1) {
|
||||
transaction.insert(iconRepositories).values(newIconRepositories).run();
|
||||
}
|
||||
|
||||
if (newIcons.length >= 1) {
|
||||
// We only insert 5000 icons at a time to avoid SQLite limitations
|
||||
for (const chunck of splitToNChunks(newIcons, Math.ceil(newIcons.length / 5000))) {
|
||||
transaction.insert(icons).values(chunck).run();
|
||||
}
|
||||
}
|
||||
if (deadIcons.length >= 1) {
|
||||
transaction
|
||||
.delete(icons)
|
||||
.where(
|
||||
inArray(
|
||||
// Combine iconRepositoryId and checksum to allow same icons on different repositories
|
||||
sql`concat(${icons.iconRepositoryId}, '.', ${icons.checksum})`,
|
||||
deadIcons.map((icon) => `${icon.iconRepositoryId}.${icon.checksum}`),
|
||||
),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
if (deadIconRepositories.length >= 1) {
|
||||
transaction
|
||||
.delete(iconRepositories)
|
||||
.where(
|
||||
inArray(
|
||||
iconRepositories.id,
|
||||
deadIconRepositories.map((iconRepository) => iconRepository.id),
|
||||
),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
countDeleted += deadIcons.length;
|
||||
});
|
||||
},
|
||||
countDeleted += deadIcons.length;
|
||||
});
|
||||
|
||||
logger.info(`Updated database within ${stopWatch.getElapsedInHumanWords()} (-${countDeleted}, +${countInserted})`);
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
import { EVERY_5_SECONDS } from "@homarr/cron-jobs-core/expressions";
|
||||
import { clusterInfoRequestHandler, systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring";
|
||||
import { systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring";
|
||||
import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler";
|
||||
|
||||
import { createCronJob } from "../../lib";
|
||||
|
||||
export const healthMonitoringJob = createCronJob("healthMonitoring", EVERY_5_SECONDS).withCallback(
|
||||
createRequestIntegrationJobHandler(
|
||||
(integration, itemOptions: Record<string, never>) => {
|
||||
const { kind } = integration;
|
||||
if (kind !== "proxmox") {
|
||||
return systemInfoRequestHandler.handler({ ...integration, kind }, itemOptions);
|
||||
}
|
||||
return clusterInfoRequestHandler.handler({ ...integration, kind }, itemOptions);
|
||||
createRequestIntegrationJobHandler(systemInfoRequestHandler.handler, {
|
||||
widgetKinds: ["healthMonitoring"],
|
||||
getInput: {
|
||||
healthMonitoring: () => ({}),
|
||||
},
|
||||
{
|
||||
widgetKinds: ["healthMonitoring"],
|
||||
getInput: {
|
||||
healthMonitoring: () => ({}),
|
||||
},
|
||||
},
|
||||
),
|
||||
}),
|
||||
);
|
||||
@@ -2,7 +2,6 @@ import Database from "better-sqlite3";
|
||||
import type { Logger } from "drizzle-orm";
|
||||
import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
|
||||
import { drizzle as drizzleSqlite } from "drizzle-orm/better-sqlite3";
|
||||
import type { MySql2Database } from "drizzle-orm/mysql2";
|
||||
import { drizzle as drizzleMysql } from "drizzle-orm/mysql2";
|
||||
import mysql from "mysql2";
|
||||
|
||||
@@ -12,8 +11,7 @@ import { env } from "./env";
|
||||
import * as mysqlSchema from "./schema/mysql";
|
||||
import * as sqliteSchema from "./schema/sqlite";
|
||||
|
||||
export type HomarrDatabase = BetterSQLite3Database<typeof sqliteSchema>;
|
||||
export type HomarrDatabaseMysql = MySql2Database<typeof mysqlSchema>;
|
||||
type HomarrDatabase = BetterSQLite3Database<typeof sqliteSchema>;
|
||||
|
||||
const init = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
|
||||
@@ -7,7 +7,5 @@ export * from "drizzle-orm";
|
||||
export const db = database;
|
||||
|
||||
export type Database = typeof db;
|
||||
export type { HomarrDatabaseMysql } from "./driver";
|
||||
|
||||
export { createId } from "@paralleldrive/cuid2";
|
||||
export { handleDiffrentDbDriverOperationsAsync as handleTransactionsAsync } from "./transactions";
|
||||
@@ -45,10 +45,10 @@
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@t3-oss/env-nextjs": "^0.11.1",
|
||||
"@testcontainers/mysql": "^10.16.0",
|
||||
"better-sqlite3": "^11.8.0",
|
||||
"better-sqlite3": "^11.7.2",
|
||||
"dotenv": "^16.4.7",
|
||||
"drizzle-kit": "^0.30.2",
|
||||
"drizzle-orm": "^0.38.4",
|
||||
"drizzle-kit": "^0.30.1",
|
||||
"drizzle-orm": "^0.38.3",
|
||||
"drizzle-zod": "^0.6.1",
|
||||
"mysql2": "3.12.0"
|
||||
},
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { HomarrDatabase, HomarrDatabaseMysql } from "./driver";
|
||||
import { env } from "./env";
|
||||
import * as mysqlSchema from "./schema/mysql";
|
||||
|
||||
type MysqlSchema = typeof mysqlSchema;
|
||||
|
||||
interface HandleTransactionInput {
|
||||
handleAsync: (db: HomarrDatabaseMysql, schema: MysqlSchema) => Promise<void>;
|
||||
handleSync: (db: HomarrDatabase) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The below method is mostly used to handle transactions in different database drivers.
|
||||
* As better-sqlite3 transactions have to be synchronous, we have to implement them in a different way.
|
||||
* But it can also generally be used when dealing with different database drivers.
|
||||
*/
|
||||
export const handleDiffrentDbDriverOperationsAsync = async (db: HomarrDatabase, input: HandleTransactionInput) => {
|
||||
if (env.DB_DRIVER !== "mysql2") {
|
||||
input.handleSync(db);
|
||||
return;
|
||||
}
|
||||
|
||||
await input.handleAsync(db as unknown as HomarrDatabaseMysql, mysqlSchema);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { HomarrDocumentationPath } from "./homarr-docs-sitemap";
|
||||
|
||||
const documentationBaseUrl = "https://homarr.dev";
|
||||
const documentationBaseUrl = "https://deploy-preview-113--homarr-docs.netlify.app";
|
||||
|
||||
// Please use the method so the path can be checked!
|
||||
export const createDocumentationLink = (path: HomarrDocumentationPath, hashTag?: `#${string}`) =>
|
||||
|
||||
@@ -5,8 +5,6 @@ export const integrationSecretKindObject = {
|
||||
apiKey: { isPublic: false },
|
||||
username: { isPublic: true },
|
||||
password: { isPublic: false },
|
||||
tokenId: { isPublic: true },
|
||||
realm: { isPublic: true },
|
||||
} satisfies Record<string, { isPublic: boolean }>;
|
||||
|
||||
export const integrationSecretKinds = objectKeys(integrationSecretKindObject);
|
||||
@@ -139,12 +137,6 @@ export const integrationDefs = {
|
||||
category: ["mediaTranscoding"],
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/tdarr.png",
|
||||
},
|
||||
proxmox: {
|
||||
name: "Proxmox",
|
||||
secretKinds: [["username", "tokenId", "apiKey", "realm"]],
|
||||
category: ["healthMonitoring"],
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/proxmox.png",
|
||||
},
|
||||
} as const satisfies Record<string, integrationDefinition>;
|
||||
|
||||
export const integrationKinds = objectKeys(integrationDefs) as AtLeastOneOf<IntegrationKind>;
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"@ctrl/deluge": "^7.1.0",
|
||||
"@ctrl/qbittorrent": "^9.2.0",
|
||||
"@ctrl/transmission": "^7.2.0",
|
||||
"@homarr/certificates": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/definitions": "workspace:^0.1.0",
|
||||
@@ -36,8 +35,6 @@
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@jellyfin/sdk": "^0.11.0",
|
||||
"proxmox-api": "1.1.1",
|
||||
"undici": "7.2.3",
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
|
||||
import { Integration } from "../base/integration";
|
||||
import { IntegrationTestConnectionError } from "../base/test-connection-error";
|
||||
import type { DnsHoleSummaryIntegration } from "../interfaces/dns-hole-summary/dns-hole-summary-integration";
|
||||
@@ -8,7 +6,7 @@ import { filteringStatusSchema, statsResponseSchema, statusResponseSchema } from
|
||||
|
||||
export class AdGuardHomeIntegration extends Integration implements DnsHoleSummaryIntegration {
|
||||
public async getSummaryAsync(): Promise<DnsHoleSummary> {
|
||||
const statsResponse = await fetchWithTrustedCertificatesAsync(this.url("/control/stats"), {
|
||||
const statsResponse = await fetch(this.url("/control/stats"), {
|
||||
headers: {
|
||||
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
|
||||
},
|
||||
@@ -20,7 +18,7 @@ export class AdGuardHomeIntegration extends Integration implements DnsHoleSummar
|
||||
);
|
||||
}
|
||||
|
||||
const statusResponse = await fetchWithTrustedCertificatesAsync(this.url("/control/status"), {
|
||||
const statusResponse = await fetch(this.url("/control/status"), {
|
||||
headers: {
|
||||
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
|
||||
},
|
||||
@@ -32,7 +30,7 @@ export class AdGuardHomeIntegration extends Integration implements DnsHoleSummar
|
||||
);
|
||||
}
|
||||
|
||||
const filteringStatusResponse = await fetchWithTrustedCertificatesAsync(this.url("/control/filtering/status"), {
|
||||
const filteringStatusResponse = await fetch(this.url("/control/filtering/status"), {
|
||||
headers: {
|
||||
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
|
||||
},
|
||||
@@ -88,7 +86,7 @@ export class AdGuardHomeIntegration extends Integration implements DnsHoleSummar
|
||||
public async testConnectionAsync(): Promise<void> {
|
||||
await super.handleTestConnectionResponseAsync({
|
||||
queryFunctionAsync: async () => {
|
||||
return await fetchWithTrustedCertificatesAsync(this.url("/control/status"), {
|
||||
return await fetch(this.url("/control/status"), {
|
||||
headers: {
|
||||
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
|
||||
},
|
||||
@@ -96,7 +94,7 @@ export class AdGuardHomeIntegration extends Integration implements DnsHoleSummar
|
||||
},
|
||||
handleResponseAsync: async (response) => {
|
||||
try {
|
||||
const result = await response.json();
|
||||
const result = (await response.json()) as unknown;
|
||||
if (typeof result === "object" && result !== null) return;
|
||||
} catch {
|
||||
throw new IntegrationTestConnectionError("invalidJson");
|
||||
@@ -108,7 +106,7 @@ export class AdGuardHomeIntegration extends Integration implements DnsHoleSummar
|
||||
}
|
||||
|
||||
public async enableAsync(): Promise<void> {
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/control/protection"), {
|
||||
const response = await fetch(this.url("/control/protection"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -126,7 +124,7 @@ export class AdGuardHomeIntegration extends Integration implements DnsHoleSummar
|
||||
}
|
||||
|
||||
public async disableAsync(duration = 0): Promise<void> {
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/control/protection"), {
|
||||
const response = await fetch(this.url("/control/protection"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -23,7 +23,6 @@ import { OverseerrIntegration } from "../overseerr/overseerr-integration";
|
||||
import { PiHoleIntegration } from "../pi-hole/pi-hole-integration";
|
||||
import { PlexIntegration } from "../plex/plex-integration";
|
||||
import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration";
|
||||
import { ProxmoxIntegration } from "../proxmox/proxmox-integration";
|
||||
import type { Integration, IntegrationInput } from "./integration";
|
||||
|
||||
export const integrationCreator = <TKind extends keyof typeof integrationCreators>(
|
||||
@@ -73,5 +72,4 @@ export const integrationCreators = {
|
||||
readarr: ReadarrIntegration,
|
||||
dashDot: DashDotIntegration,
|
||||
tdarr: TdarrIntegration,
|
||||
proxmox: ProxmoxIntegration,
|
||||
} satisfies Record<IntegrationKind, new (integration: IntegrationInput) => Integration>;
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { Response } from "undici";
|
||||
|
||||
import { extractErrorMessage, removeTrailingSlash } from "@homarr/common";
|
||||
import type { IntegrationSecretKind } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
@@ -4,7 +4,6 @@ import "@homarr/redis";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { z } from "@homarr/validation";
|
||||
|
||||
import { createChannelEventHistory } from "../../../redis/src/lib/channel";
|
||||
@@ -13,7 +12,7 @@ import type { HealthMonitoring } from "../types";
|
||||
|
||||
export class DashDotIntegration extends Integration {
|
||||
public async testConnectionAsync(): Promise<void> {
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/info"));
|
||||
const response = await fetch(this.url("/info"));
|
||||
await response.json();
|
||||
}
|
||||
|
||||
@@ -53,7 +52,7 @@ export class DashDotIntegration extends Integration {
|
||||
}
|
||||
|
||||
private async getInfoAsync() {
|
||||
const infoResponse = await fetchWithTrustedCertificatesAsync(this.url("/info"));
|
||||
const infoResponse = await fetch(this.url("/info"));
|
||||
const serverInfo = await internalServerInfoApi.parseAsync(await infoResponse.json());
|
||||
return {
|
||||
maxAvailableMemoryBytes: serverInfo.ram.size,
|
||||
@@ -67,7 +66,7 @@ export class DashDotIntegration extends Integration {
|
||||
|
||||
private async getCurrentCpuLoadAsync() {
|
||||
const channel = this.getChannel();
|
||||
const cpu = await fetchWithTrustedCertificatesAsync(this.url("/load/cpu"));
|
||||
const cpu = await fetch(this.url("/load/cpu"));
|
||||
const data = await cpuLoadPerCoreApiList.parseAsync(await cpu.json());
|
||||
await channel.pushAsync(data);
|
||||
return {
|
||||
@@ -89,12 +88,12 @@ export class DashDotIntegration extends Integration {
|
||||
}
|
||||
|
||||
private async getCurrentStorageLoadAsync() {
|
||||
const storageLoad = await fetchWithTrustedCertificatesAsync(this.url("/load/storage"));
|
||||
const storageLoad = await fetch(this.url("/load/storage"));
|
||||
return (await storageLoad.json()) as number[];
|
||||
}
|
||||
|
||||
private async getCurrentMemoryLoadAsync() {
|
||||
const memoryLoad = await fetchWithTrustedCertificatesAsync(this.url("/load/ram"));
|
||||
const memoryLoad = await fetch(this.url("/load/ram"));
|
||||
const data = await memoryLoadApi.parseAsync(await memoryLoad.json());
|
||||
return {
|
||||
loadInBytes: data.load,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Deluge } from "@ctrl/deluge";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { createCertificateAgentAsync } from "@homarr/certificates/server";
|
||||
|
||||
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
|
||||
import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
|
||||
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
|
||||
@@ -10,13 +8,13 @@ import type { DownloadClientStatus } from "../../interfaces/downloads/download-c
|
||||
|
||||
export class DelugeIntegration extends DownloadClientIntegration {
|
||||
public async testConnectionAsync(): Promise<void> {
|
||||
const client = await this.getClientAsync();
|
||||
const client = this.getClient();
|
||||
await client.login();
|
||||
}
|
||||
|
||||
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
|
||||
const type = "torrent";
|
||||
const client = await this.getClientAsync();
|
||||
const client = this.getClient();
|
||||
const {
|
||||
stats: { download_rate, upload_rate },
|
||||
torrents: rawTorrents,
|
||||
@@ -59,7 +57,7 @@ export class DelugeIntegration extends DownloadClientIntegration {
|
||||
}
|
||||
|
||||
public async pauseQueueAsync() {
|
||||
const client = await this.getClientAsync();
|
||||
const client = this.getClient();
|
||||
const store = (await client.listTorrents()).result.torrents;
|
||||
await Promise.all(
|
||||
Object.entries(store).map(async ([id]) => {
|
||||
@@ -69,12 +67,11 @@ export class DelugeIntegration extends DownloadClientIntegration {
|
||||
}
|
||||
|
||||
public async pauseItemAsync({ id }: DownloadClientItem): Promise<void> {
|
||||
const client = await this.getClientAsync();
|
||||
await client.pauseTorrent(id);
|
||||
await this.getClient().pauseTorrent(id);
|
||||
}
|
||||
|
||||
public async resumeQueueAsync() {
|
||||
const client = await this.getClientAsync();
|
||||
const client = this.getClient();
|
||||
const store = (await client.listTorrents()).result.torrents;
|
||||
await Promise.all(
|
||||
Object.entries(store).map(async ([id]) => {
|
||||
@@ -84,20 +81,17 @@ export class DelugeIntegration extends DownloadClientIntegration {
|
||||
}
|
||||
|
||||
public async resumeItemAsync({ id }: DownloadClientItem): Promise<void> {
|
||||
const client = await this.getClientAsync();
|
||||
await client.resumeTorrent(id);
|
||||
await this.getClient().resumeTorrent(id);
|
||||
}
|
||||
|
||||
public async deleteItemAsync({ id }: DownloadClientItem, fromDisk: boolean): Promise<void> {
|
||||
const client = await this.getClientAsync();
|
||||
await client.removeTorrent(id, fromDisk);
|
||||
await this.getClient().removeTorrent(id, fromDisk);
|
||||
}
|
||||
|
||||
private async getClientAsync() {
|
||||
private getClient() {
|
||||
return new Deluge({
|
||||
baseUrl: this.url("/").toString(),
|
||||
password: this.getSecretValue("password"),
|
||||
dispatcher: await createCertificateAgentAsync(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
|
||||
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
|
||||
import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
|
||||
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
|
||||
@@ -98,7 +96,7 @@ export class NzbGetIntegration extends DownloadClientIntegration {
|
||||
const password = this.getSecretValue("password");
|
||||
const url = this.url(`/${username}:${password}/jsonrpc`);
|
||||
const body = JSON.stringify({ method, params });
|
||||
return await fetchWithTrustedCertificatesAsync(url, { method: "POST", body })
|
||||
return await fetch(url, { method: "POST", body })
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { QBittorrent } from "@ctrl/qbittorrent";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { createCertificateAgentAsync } from "@homarr/certificates/server";
|
||||
|
||||
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
|
||||
import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
|
||||
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
|
||||
@@ -10,13 +8,13 @@ import type { DownloadClientStatus } from "../../interfaces/downloads/download-c
|
||||
|
||||
export class QBitTorrentIntegration extends DownloadClientIntegration {
|
||||
public async testConnectionAsync(): Promise<void> {
|
||||
const client = await this.getClientAsync();
|
||||
const client = this.getClient();
|
||||
await client.login();
|
||||
}
|
||||
|
||||
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
|
||||
const type = "torrent";
|
||||
const client = await this.getClientAsync();
|
||||
const client = this.getClient();
|
||||
const torrents = await client.listTorrents();
|
||||
const rates = torrents.reduce(
|
||||
({ down, up }, { dlspeed, upspeed }) => ({ down: down + dlspeed, up: up + upspeed }),
|
||||
@@ -52,36 +50,30 @@ export class QBitTorrentIntegration extends DownloadClientIntegration {
|
||||
}
|
||||
|
||||
public async pauseQueueAsync() {
|
||||
const client = await this.getClientAsync();
|
||||
await client.pauseTorrent("all");
|
||||
await this.getClient().pauseTorrent("all");
|
||||
}
|
||||
|
||||
public async pauseItemAsync({ id }: DownloadClientItem): Promise<void> {
|
||||
const client = await this.getClientAsync();
|
||||
await client.pauseTorrent(id);
|
||||
await this.getClient().pauseTorrent(id);
|
||||
}
|
||||
|
||||
public async resumeQueueAsync() {
|
||||
const client = await this.getClientAsync();
|
||||
await client.resumeTorrent("all");
|
||||
await this.getClient().resumeTorrent("all");
|
||||
}
|
||||
|
||||
public async resumeItemAsync({ id }: DownloadClientItem): Promise<void> {
|
||||
const client = await this.getClientAsync();
|
||||
await client.resumeTorrent(id);
|
||||
await this.getClient().resumeTorrent(id);
|
||||
}
|
||||
|
||||
public async deleteItemAsync({ id }: DownloadClientItem, fromDisk: boolean): Promise<void> {
|
||||
const client = await this.getClientAsync();
|
||||
await client.removeTorrent(id, fromDisk);
|
||||
await this.getClient().removeTorrent(id, fromDisk);
|
||||
}
|
||||
|
||||
private async getClientAsync() {
|
||||
private getClient() {
|
||||
return new QBittorrent({
|
||||
baseUrl: this.url("/").toString(),
|
||||
username: this.getSecretValue("username"),
|
||||
password: this.getSecretValue("password"),
|
||||
dispatcher: await createCertificateAgentAsync(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import dayjs from "dayjs";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
|
||||
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
|
||||
import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
|
||||
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
|
||||
@@ -108,12 +106,12 @@ export class SabnzbdIntegration extends DownloadClientIntegration {
|
||||
apikey: this.getSecretValue("apiKey"),
|
||||
});
|
||||
|
||||
return await fetchWithTrustedCertificatesAsync(url)
|
||||
return await fetch(url)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
return response.json();
|
||||
return response.json() as Promise<unknown>;
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error instanceof Error) {
|
||||
|
||||
+10
-17
@@ -1,8 +1,6 @@
|
||||
import { Transmission } from "@ctrl/transmission";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { createCertificateAgentAsync } from "@homarr/certificates/server";
|
||||
|
||||
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
|
||||
import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
|
||||
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
|
||||
@@ -10,13 +8,12 @@ import type { DownloadClientStatus } from "../../interfaces/downloads/download-c
|
||||
|
||||
export class TransmissionIntegration extends DownloadClientIntegration {
|
||||
public async testConnectionAsync(): Promise<void> {
|
||||
const client = await this.getClientAsync();
|
||||
await client.getSession();
|
||||
await this.getClient().getSession();
|
||||
}
|
||||
|
||||
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
|
||||
const type = "torrent";
|
||||
const client = await this.getClientAsync();
|
||||
const client = this.getClient();
|
||||
const { torrents } = (await client.listTorrents()).arguments;
|
||||
const rates = torrents.reduce(
|
||||
({ down, up }, { rateDownload, rateUpload }) => ({ down: down + rateDownload, up: up + rateUpload }),
|
||||
@@ -50,38 +47,34 @@ export class TransmissionIntegration extends DownloadClientIntegration {
|
||||
}
|
||||
|
||||
public async pauseQueueAsync() {
|
||||
const client = await this.getClientAsync();
|
||||
const client = this.getClient();
|
||||
const ids = (await client.listTorrents()).arguments.torrents.map(({ hashString }) => hashString);
|
||||
await client.pauseTorrent(ids);
|
||||
await this.getClient().pauseTorrent(ids);
|
||||
}
|
||||
|
||||
public async pauseItemAsync({ id }: DownloadClientItem): Promise<void> {
|
||||
const client = await this.getClientAsync();
|
||||
await client.pauseTorrent(id);
|
||||
await this.getClient().pauseTorrent(id);
|
||||
}
|
||||
|
||||
public async resumeQueueAsync() {
|
||||
const client = await this.getClientAsync();
|
||||
const client = this.getClient();
|
||||
const ids = (await client.listTorrents()).arguments.torrents.map(({ hashString }) => hashString);
|
||||
await client.resumeTorrent(ids);
|
||||
await this.getClient().resumeTorrent(ids);
|
||||
}
|
||||
|
||||
public async resumeItemAsync({ id }: DownloadClientItem): Promise<void> {
|
||||
const client = await this.getClientAsync();
|
||||
await client.resumeTorrent(id);
|
||||
await this.getClient().resumeTorrent(id);
|
||||
}
|
||||
|
||||
public async deleteItemAsync({ id }: DownloadClientItem, fromDisk: boolean): Promise<void> {
|
||||
const client = await this.getClientAsync();
|
||||
await client.removeTorrent(id, fromDisk);
|
||||
await this.getClient().removeTorrent(id, fromDisk);
|
||||
}
|
||||
|
||||
private async getClientAsync() {
|
||||
private getClient() {
|
||||
return new Transmission({
|
||||
baseUrl: this.url("/").toString(),
|
||||
username: this.getSecretValue("username"),
|
||||
password: this.getSecretValue("password"),
|
||||
dispatcher: await createCertificateAgentAsync(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { Integration } from "../base/integration";
|
||||
@@ -8,7 +7,7 @@ export class HomeAssistantIntegration extends Integration {
|
||||
public async getEntityStateAsync(entityId: string) {
|
||||
try {
|
||||
const response = await this.getAsync(`/api/states/${entityId}`);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as unknown;
|
||||
if (!response.ok) {
|
||||
logger.warn(`Response did not indicate success`);
|
||||
return {
|
||||
@@ -72,7 +71,7 @@ export class HomeAssistantIntegration extends Integration {
|
||||
* @returns the response from the API
|
||||
*/
|
||||
private async getAsync(path: `/api/${string}`) {
|
||||
return await fetchWithTrustedCertificatesAsync(this.url(path), {
|
||||
return await fetch(this.url(path), {
|
||||
headers: this.getAuthHeaders(),
|
||||
});
|
||||
}
|
||||
@@ -85,7 +84,7 @@ export class HomeAssistantIntegration extends Integration {
|
||||
* @returns the response from the API
|
||||
*/
|
||||
private async postAsync(path: `/api/${string}`, body: Record<string, string>) {
|
||||
return await fetchWithTrustedCertificatesAsync(this.url(path), {
|
||||
return await fetch(this.url(path), {
|
||||
headers: this.getAuthHeaders(),
|
||||
body: JSON.stringify(body),
|
||||
method: "POST",
|
||||
|
||||
@@ -2,8 +2,6 @@ import { Jellyfin } from "@jellyfin/sdk";
|
||||
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
|
||||
import { getSystemApi } from "@jellyfin/sdk/lib/utils/api/system-api";
|
||||
|
||||
import { createAxiosCertificateInstanceAsync } from "@homarr/certificates/server";
|
||||
|
||||
import { Integration } from "../base/integration";
|
||||
import type { StreamSession } from "../interfaces/media-server/session";
|
||||
|
||||
@@ -67,13 +65,12 @@ export class JellyfinIntegration extends Integration {
|
||||
* @returns An instance of Api that has been authenticated
|
||||
*/
|
||||
private async getApiAsync() {
|
||||
const httpsAgent = await createAxiosCertificateInstanceAsync();
|
||||
if (this.hasSecretValue("apiKey")) {
|
||||
const apiKey = this.getSecretValue("apiKey");
|
||||
return this.jellyfin.createApi(this.url("/").toString(), apiKey, httpsAgent);
|
||||
return this.jellyfin.createApi(this.url("/").toString(), apiKey);
|
||||
}
|
||||
|
||||
const apiClient = this.jellyfin.createApi(this.url("/").toString(), undefined, httpsAgent);
|
||||
const apiClient = this.jellyfin.createApi(this.url("/").toString());
|
||||
// Authentication state is stored internally in the Api class, so now
|
||||
// requests that require authentication can be made normally.
|
||||
// see https://typescript-sdk.jellyfin.org/#usage
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { logger } from "@homarr/log";
|
||||
import { z } from "@homarr/validation";
|
||||
|
||||
@@ -9,7 +8,7 @@ export class LidarrIntegration extends MediaOrganizerIntegration {
|
||||
public async testConnectionAsync(): Promise<void> {
|
||||
await super.handleTestConnectionResponseAsync({
|
||||
queryFunctionAsync: async () => {
|
||||
return await fetchWithTrustedCertificatesAsync(this.url("/api"), {
|
||||
return await fetch(this.url("/api"), {
|
||||
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
||||
});
|
||||
},
|
||||
@@ -29,7 +28,7 @@ export class LidarrIntegration extends MediaOrganizerIntegration {
|
||||
unmonitored: includeUnmonitored,
|
||||
});
|
||||
|
||||
const response = await fetchWithTrustedCertificatesAsync(url, {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"X-Api-Key": super.getSecretValue("apiKey"),
|
||||
},
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import type { AtLeastOneOf } from "@homarr/common/types";
|
||||
import { logger } from "@homarr/log";
|
||||
import { z } from "@homarr/validation";
|
||||
@@ -21,7 +20,7 @@ export class RadarrIntegration extends MediaOrganizerIntegration {
|
||||
unmonitored: includeUnmonitored,
|
||||
});
|
||||
|
||||
const response = await fetchWithTrustedCertificatesAsync(url, {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"X-Api-Key": super.getSecretValue("apiKey"),
|
||||
},
|
||||
@@ -95,7 +94,7 @@ export class RadarrIntegration extends MediaOrganizerIntegration {
|
||||
public async testConnectionAsync(): Promise<void> {
|
||||
await super.handleTestConnectionResponseAsync({
|
||||
queryFunctionAsync: async () => {
|
||||
return await fetchWithTrustedCertificatesAsync(this.url("/api"), {
|
||||
return await fetch(this.url("/api"), {
|
||||
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { logger } from "@homarr/log";
|
||||
import { z } from "@homarr/validation";
|
||||
|
||||
@@ -9,7 +8,7 @@ export class ReadarrIntegration extends MediaOrganizerIntegration {
|
||||
public async testConnectionAsync(): Promise<void> {
|
||||
await super.handleTestConnectionResponseAsync({
|
||||
queryFunctionAsync: async () => {
|
||||
return await fetchWithTrustedCertificatesAsync(this.url("/api"), {
|
||||
return await fetch(this.url("/api"), {
|
||||
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
||||
});
|
||||
},
|
||||
@@ -35,7 +34,7 @@ export class ReadarrIntegration extends MediaOrganizerIntegration {
|
||||
includeAuthor,
|
||||
});
|
||||
|
||||
const response = await fetchWithTrustedCertificatesAsync(url, {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"X-Api-Key": super.getSecretValue("apiKey"),
|
||||
},
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { logger } from "@homarr/log";
|
||||
import { z } from "@homarr/validation";
|
||||
|
||||
@@ -22,7 +21,7 @@ export class SonarrIntegration extends MediaOrganizerIntegration {
|
||||
includeEpisodeImages: true,
|
||||
});
|
||||
|
||||
const response = await fetchWithTrustedCertificatesAsync(url, {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"X-Api-Key": super.getSecretValue("apiKey"),
|
||||
},
|
||||
@@ -94,7 +93,7 @@ export class SonarrIntegration extends MediaOrganizerIntegration {
|
||||
public async testConnectionAsync(): Promise<void> {
|
||||
await super.handleTestConnectionResponseAsync({
|
||||
queryFunctionAsync: async () => {
|
||||
return await fetchWithTrustedCertificatesAsync(this.url("/api"), {
|
||||
return await fetch(this.url("/api"), {
|
||||
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { z } from "@homarr/validation";
|
||||
|
||||
import { Integration } from "../base/integration";
|
||||
@@ -10,7 +9,7 @@ import { getNodesResponseSchema, getStatisticsSchema, getStatusTableSchema } fro
|
||||
export class TdarrIntegration extends Integration {
|
||||
public async testConnectionAsync(): Promise<void> {
|
||||
const url = this.url("/api/v2/status");
|
||||
const response = await fetchWithTrustedCertificatesAsync(url);
|
||||
const response = await fetch(url);
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Unexpected status code: ${response.status}`);
|
||||
}
|
||||
@@ -20,7 +19,7 @@ export class TdarrIntegration extends Integration {
|
||||
|
||||
public async getStatisticsAsync(): Promise<TdarrStatistics> {
|
||||
const url = this.url("/api/v2/cruddb");
|
||||
const response = await fetchWithTrustedCertificatesAsync(url, {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
@@ -62,7 +61,7 @@ export class TdarrIntegration extends Integration {
|
||||
|
||||
public async getWorkersAsync(): Promise<TdarrWorker[]> {
|
||||
const url = this.url("/api/v2/get-nodes");
|
||||
const response = await fetchWithTrustedCertificatesAsync(url, {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
@@ -102,7 +101,7 @@ export class TdarrIntegration extends Integration {
|
||||
|
||||
private async getTranscodingQueueAsync(firstItemIndex: number, pageSize: number) {
|
||||
const url = this.url("/api/v2/client/status-tables");
|
||||
const response = await fetchWithTrustedCertificatesAsync(url, {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
@@ -137,7 +136,7 @@ export class TdarrIntegration extends Integration {
|
||||
|
||||
private async getHealthCheckDataAsync(firstItemIndex: number, pageSize: number, totalQueueCount: number) {
|
||||
const url = this.url("/api/v2/client/status-tables");
|
||||
const response = await fetchWithTrustedCertificatesAsync(url, {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import type { Response } from "undici";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
|
||||
import { Integration } from "../base/integration";
|
||||
import { IntegrationTestConnectionError } from "../base/test-connection-error";
|
||||
import type { HealthMonitoring } from "../types";
|
||||
@@ -109,7 +105,7 @@ export class OpenMediaVaultIntegration extends Integration {
|
||||
if (!response.ok) {
|
||||
throw new IntegrationTestConnectionError("invalidCredentials");
|
||||
}
|
||||
const result = await response.json();
|
||||
const result = (await response.json()) as unknown;
|
||||
if (typeof result !== "object" || result === null || !("response" in result)) {
|
||||
throw new IntegrationTestConnectionError("invalidJson");
|
||||
}
|
||||
@@ -121,7 +117,7 @@ export class OpenMediaVaultIntegration extends Integration {
|
||||
params: Record<string, unknown>,
|
||||
headers: Record<string, string> = {},
|
||||
): Promise<Response> {
|
||||
return await fetchWithTrustedCertificatesAsync(this.url("/rpc.php"), {
|
||||
return await fetch(this.url("/rpc.php"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { logger } from "@homarr/log";
|
||||
import { z } from "@homarr/validation";
|
||||
|
||||
@@ -21,7 +20,7 @@ interface OverseerrSearchResult {
|
||||
*/
|
||||
export class OverseerrIntegration extends Integration implements ISearchableIntegration<OverseerrSearchResult> {
|
||||
public async searchAsync(query: string) {
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/search", { query }), {
|
||||
const response = await fetch(this.url("/api/v1/search", { query }), {
|
||||
headers: {
|
||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||
},
|
||||
@@ -45,7 +44,7 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
||||
|
||||
public async getSeriesInformationAsync(mediaType: "movie" | "tv", id: number) {
|
||||
const url = mediaType === "tv" ? this.url(`/api/v1/tv/${id}`) : this.url(`/api/v1/movie/${id}`);
|
||||
const response = await fetchWithTrustedCertificatesAsync(url, {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||
},
|
||||
@@ -61,7 +60,7 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
||||
*/
|
||||
public async requestMediaAsync(mediaType: "movie" | "tv", id: number, seasons?: number[]): Promise<void> {
|
||||
const url = this.url("/api/v1/request");
|
||||
const response = await fetchWithTrustedCertificatesAsync(url, {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
mediaType,
|
||||
@@ -81,12 +80,13 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
||||
}
|
||||
|
||||
public async testConnectionAsync(): Promise<void> {
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/auth/me"), {
|
||||
const response = await fetch(this.url("/api/v1/auth/me"), {
|
||||
headers: {
|
||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||
},
|
||||
});
|
||||
const json = (await response.json()) as object;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const json: object = await response.json();
|
||||
if (Object.keys(json).includes("id")) {
|
||||
return;
|
||||
}
|
||||
@@ -96,17 +96,14 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
||||
|
||||
public async getRequestsAsync(): Promise<MediaRequest[]> {
|
||||
//Ensure to get all pending request first
|
||||
const pendingRequests = await fetchWithTrustedCertificatesAsync(
|
||||
this.url("/api/v1/request", { take: -1, filter: "pending" }),
|
||||
{
|
||||
headers: {
|
||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||
},
|
||||
const pendingRequests = await fetch(this.url("/api/v1/request", { take: -1, filter: "pending" }), {
|
||||
headers: {
|
||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
//Change 20 to integration setting (set to -1 for all)
|
||||
const allRequests = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/request", { take: 20 }), {
|
||||
const allRequests = await fetch(this.url("/api/v1/request", { take: 20 }), {
|
||||
headers: {
|
||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||
},
|
||||
@@ -154,7 +151,7 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
||||
}
|
||||
|
||||
public async getStatsAsync(): Promise<RequestStats> {
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/request/count"), {
|
||||
const response = await fetch(this.url("/api/v1/request/count"), {
|
||||
headers: {
|
||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||
},
|
||||
@@ -163,7 +160,7 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
||||
}
|
||||
|
||||
public async getUsersAsync(): Promise<RequestUser[]> {
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/user", { take: -1 }), {
|
||||
const response = await fetch(this.url("/api/v1/user", { take: -1 }), {
|
||||
headers: {
|
||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||
},
|
||||
@@ -180,7 +177,7 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
||||
|
||||
public async approveRequestAsync(requestId: number): Promise<void> {
|
||||
logger.info(`Approving media request id='${requestId}' integration='${this.integration.name}'`);
|
||||
await fetchWithTrustedCertificatesAsync(this.url(`/api/v1/request/${requestId}/approve`), {
|
||||
await fetch(this.url(`/api/v1/request/${requestId}/approve`), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||
@@ -198,7 +195,7 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
||||
|
||||
public async declineRequestAsync(requestId: number): Promise<void> {
|
||||
logger.info(`Declining media request id='${requestId}' integration='${this.integration.name}'`);
|
||||
await fetchWithTrustedCertificatesAsync(this.url(`/api/v1/request/${requestId}/decline`), {
|
||||
await fetch(this.url(`/api/v1/request/${requestId}/decline`), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||
@@ -215,7 +212,7 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
||||
}
|
||||
|
||||
private async getItemInformationAsync(id: number, type: MediaRequest["type"]): Promise<MediaInformation> {
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url(`/api/v1/${type}/${id}`), {
|
||||
const response = await fetch(this.url(`/api/v1/${type}/${id}`), {
|
||||
headers: {
|
||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||
},
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
|
||||
import { Integration } from "../base/integration";
|
||||
import { IntegrationTestConnectionError } from "../base/test-connection-error";
|
||||
import type { DnsHoleSummaryIntegration } from "../interfaces/dns-hole-summary/dns-hole-summary-integration";
|
||||
@@ -9,7 +7,7 @@ import { summaryResponseSchema } from "./pi-hole-types";
|
||||
export class PiHoleIntegration extends Integration implements DnsHoleSummaryIntegration {
|
||||
public async getSummaryAsync(): Promise<DnsHoleSummary> {
|
||||
const apiKey = super.getSecretValue("apiKey");
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/admin/api.php?summaryRaw", { auth: apiKey }));
|
||||
const response = await fetch(this.url("/admin/api.php?summaryRaw", { auth: apiKey }));
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch summary for ${this.integration.name} (${this.integration.id}): ${response.statusText}`,
|
||||
@@ -38,11 +36,11 @@ export class PiHoleIntegration extends Integration implements DnsHoleSummaryInte
|
||||
|
||||
await super.handleTestConnectionResponseAsync({
|
||||
queryFunctionAsync: async () => {
|
||||
return await fetchWithTrustedCertificatesAsync(this.url("/admin/api.php?status", { auth: apiKey }));
|
||||
return await fetch(this.url("/admin/api.php?status", { auth: apiKey }));
|
||||
},
|
||||
handleResponseAsync: async (response) => {
|
||||
try {
|
||||
const result = await response.json();
|
||||
const result = (await response.json()) as unknown;
|
||||
if (typeof result === "object" && result !== null && "status" in result) return;
|
||||
} catch {
|
||||
throw new IntegrationTestConnectionError("invalidJson");
|
||||
@@ -55,7 +53,7 @@ export class PiHoleIntegration extends Integration implements DnsHoleSummaryInte
|
||||
|
||||
public async enableAsync(): Promise<void> {
|
||||
const apiKey = super.getSecretValue("apiKey");
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/admin/api.php?enable", { auth: apiKey }));
|
||||
const response = await fetch(this.url("/admin/api.php?enable", { auth: apiKey }));
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to enable PiHole for ${this.integration.name} (${this.integration.id}): ${response.statusText}`,
|
||||
@@ -66,7 +64,7 @@ export class PiHoleIntegration extends Integration implements DnsHoleSummaryInte
|
||||
public async disableAsync(duration?: number): Promise<void> {
|
||||
const apiKey = super.getSecretValue("apiKey");
|
||||
const url = this.url(`/admin/api.php?disable${duration ? `=${duration}` : ""}`, { auth: apiKey });
|
||||
const response = await fetchWithTrustedCertificatesAsync(url);
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to disable PiHole for ${this.integration.name} (${this.integration.id}): ${response.statusText}`,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { parseStringPromise } from "xml2js";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { Integration } from "../base/integration";
|
||||
@@ -12,7 +11,7 @@ export class PlexIntegration extends Integration {
|
||||
public async getCurrentSessionsAsync(): Promise<StreamSession[]> {
|
||||
const token = super.getSecretValue("apiKey");
|
||||
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/status/sessions"), {
|
||||
const response = await fetch(this.url("/status/sessions"), {
|
||||
headers: {
|
||||
"X-Plex-Token": token,
|
||||
},
|
||||
@@ -67,7 +66,7 @@ export class PlexIntegration extends Integration {
|
||||
|
||||
await super.handleTestConnectionResponseAsync({
|
||||
queryFunctionAsync: async () => {
|
||||
return await fetchWithTrustedCertificatesAsync(this.url("/"), {
|
||||
return await fetch(this.url("/"), {
|
||||
headers: {
|
||||
"X-Plex-Token": token,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
|
||||
import { Integration } from "../base/integration";
|
||||
import { IntegrationTestConnectionError } from "../base/test-connection-error";
|
||||
import type { Indexer } from "../interfaces/indexer-manager/indexer";
|
||||
@@ -9,7 +7,7 @@ export class ProwlarrIntegration extends Integration {
|
||||
public async getIndexersAsync(): Promise<Indexer[]> {
|
||||
const apiKey = super.getSecretValue("apiKey");
|
||||
|
||||
const indexerResponse = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/indexer"), {
|
||||
const indexerResponse = await fetch(this.url("/api/v1/indexer"), {
|
||||
headers: {
|
||||
"X-Api-Key": apiKey,
|
||||
},
|
||||
@@ -20,7 +18,7 @@ export class ProwlarrIntegration extends Integration {
|
||||
);
|
||||
}
|
||||
|
||||
const statusResponse = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/indexerstatus"), {
|
||||
const statusResponse = await fetch(this.url("/api/v1/indexerstatus"), {
|
||||
headers: {
|
||||
"X-Api-Key": apiKey,
|
||||
},
|
||||
@@ -62,7 +60,7 @@ export class ProwlarrIntegration extends Integration {
|
||||
|
||||
public async testAllAsync(): Promise<void> {
|
||||
const apiKey = super.getSecretValue("apiKey");
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/indexer/testall"), {
|
||||
const response = await fetch(this.url("/api/v1/indexer/testall"), {
|
||||
headers: {
|
||||
"X-Api-Key": apiKey,
|
||||
},
|
||||
@@ -80,7 +78,7 @@ export class ProwlarrIntegration extends Integration {
|
||||
|
||||
await super.handleTestConnectionResponseAsync({
|
||||
queryFunctionAsync: async () => {
|
||||
return await fetchWithTrustedCertificatesAsync(this.url("/api"), {
|
||||
return await fetch(this.url("/api"), {
|
||||
headers: {
|
||||
"X-Api-Key": apiKey,
|
||||
},
|
||||
@@ -88,7 +86,7 @@ export class ProwlarrIntegration extends Integration {
|
||||
},
|
||||
handleResponseAsync: async (response) => {
|
||||
try {
|
||||
const result = await response.json();
|
||||
const result = (await response.json()) as unknown;
|
||||
if (typeof result === "object" && result !== null) return;
|
||||
} catch {
|
||||
throw new IntegrationTestConnectionError("invalidJson");
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import type { Proxmox } from "proxmox-api";
|
||||
import proxmoxApi from "proxmox-api";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { extractErrorMessage } from "@homarr/common";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { Integration } from "../base/integration";
|
||||
import { IntegrationTestConnectionError } from "../base/test-connection-error";
|
||||
import type {
|
||||
ComputeResourceBase,
|
||||
LxcResource,
|
||||
NodeResource,
|
||||
QemuResource,
|
||||
Resource,
|
||||
StorageResource,
|
||||
} from "./proxmox-types";
|
||||
|
||||
export class ProxmoxIntegration extends Integration {
|
||||
public async testConnectionAsync(): Promise<void> {
|
||||
const proxmox = this.getPromoxApi();
|
||||
await proxmox.nodes.$get().catch((error) => {
|
||||
throw new IntegrationTestConnectionError("internalServerError", extractErrorMessage(error));
|
||||
});
|
||||
}
|
||||
|
||||
public async getClusterInfoAsync() {
|
||||
const proxmox = this.getPromoxApi();
|
||||
const resources = await proxmox.cluster.resources.$get();
|
||||
|
||||
logger.info(
|
||||
`Found ${resources.length} resources in Proxmox cluster node=${resources.filter((resource) => resource.type === "node").length} lxc=${resources.filter((resource) => resource.type === "lxc").length} qemu=${resources.filter((resource) => resource.type === "qemu").length} storage=${resources.filter((resource) => resource.type === "storage").length}`,
|
||||
);
|
||||
|
||||
const mappedResources = resources.map(mapResource).filter((resource) => resource !== null);
|
||||
return {
|
||||
nodes: mappedResources.filter((resource): resource is NodeResource => resource.type === "node"),
|
||||
lxcs: mappedResources.filter((resource): resource is LxcResource => resource.type === "lxc"),
|
||||
vms: mappedResources.filter((resource): resource is QemuResource => resource.type === "qemu"),
|
||||
storages: mappedResources.filter((resource): resource is StorageResource => resource.type === "storage"),
|
||||
};
|
||||
}
|
||||
|
||||
private getPromoxApi() {
|
||||
return proxmoxApi({
|
||||
host: this.url("/").host,
|
||||
tokenID: `${this.getSecretValue("username")}@${this.getSecretValue("realm")}!${this.getSecretValue("tokenId")}`,
|
||||
tokenSecret: this.getSecretValue("apiKey"),
|
||||
fetch: fetchWithTrustedCertificatesAsync,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const mapResource = (resource: Proxmox.clusterResourcesResources): Resource | null => {
|
||||
switch (resource.type) {
|
||||
case "node":
|
||||
return mapNodeResource(resource);
|
||||
case "lxc":
|
||||
case "qemu":
|
||||
return mapVmResource(resource);
|
||||
case "storage":
|
||||
return mapStorageResource(resource);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const mapComputeResource = (resource: Proxmox.clusterResourcesResources): Omit<ComputeResourceBase<string>, "type"> => {
|
||||
return {
|
||||
cpu: {
|
||||
utilization: resource.cpu ?? 0,
|
||||
cores: resource.maxcpu ?? 0,
|
||||
},
|
||||
memory: {
|
||||
used: resource.mem ?? 0,
|
||||
total: resource.maxmem ?? 0,
|
||||
},
|
||||
storage: {
|
||||
used: resource.disk ?? 0,
|
||||
total: resource.maxdisk ?? 0,
|
||||
read: (resource.diskread as number | null) ?? null,
|
||||
write: (resource.diskwrite as number | null) ?? null,
|
||||
},
|
||||
network: {
|
||||
in: (resource.netin as number | null) ?? null,
|
||||
out: (resource.netout as number | null) ?? null,
|
||||
},
|
||||
haState: resource.hastate ?? null,
|
||||
isRunning: resource.status === "running" || resource.status === "online",
|
||||
name: resource.name ?? "",
|
||||
node: resource.node ?? "",
|
||||
status: resource.status ?? (resource.type === "node" ? "offline" : "stopped"),
|
||||
uptime: resource.uptime ?? 0,
|
||||
};
|
||||
};
|
||||
|
||||
const mapNodeResource = (resource: Proxmox.clusterResourcesResources): NodeResource => {
|
||||
return {
|
||||
type: "node",
|
||||
...mapComputeResource(resource),
|
||||
name: resource.node ?? "",
|
||||
};
|
||||
};
|
||||
|
||||
const mapVmResource = (resource: Proxmox.clusterResourcesResources): LxcResource | QemuResource => {
|
||||
return {
|
||||
type: resource.type as "lxc" | "qemu",
|
||||
vmId: resource.vmid ?? 0,
|
||||
...mapComputeResource(resource),
|
||||
};
|
||||
};
|
||||
|
||||
const mapStorageResource = (resource: Proxmox.clusterResourcesResources): StorageResource => {
|
||||
return {
|
||||
type: "storage",
|
||||
name: resource.storage ?? "",
|
||||
node: resource.node ?? "",
|
||||
isRunning: resource.status === "available",
|
||||
status: resource.status ?? "offline",
|
||||
storagePlugin: resource.storage ?? "",
|
||||
total: resource.maxdisk ?? 0,
|
||||
used: resource.disk ?? 0,
|
||||
isShared: resource.shared === 1,
|
||||
};
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
interface ResourceBase<TType extends string> {
|
||||
type: TType;
|
||||
name: string;
|
||||
node: string;
|
||||
isRunning: boolean;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface ComputeResourceBase<TType extends string> extends ResourceBase<TType> {
|
||||
cpu: {
|
||||
utilization: number; // previously cpu (0-1)
|
||||
cores: number; // previously cpuCores
|
||||
};
|
||||
memory: {
|
||||
used: number; // previously mem
|
||||
total: number; // previously maxMem
|
||||
};
|
||||
storage: {
|
||||
used: number; // previously disk
|
||||
total: number; // previously maxDisk
|
||||
read: number | null; // previously diskRead
|
||||
write: number | null; // previously diskWrite
|
||||
};
|
||||
network: {
|
||||
in: number | null; // previously netIn
|
||||
out: number | null; // previously netOut
|
||||
};
|
||||
uptime: number; // expressed in seconds
|
||||
haState: string | null; // HA service status (for HA managed VMs).
|
||||
}
|
||||
|
||||
export type NodeResource = ComputeResourceBase<"node">;
|
||||
|
||||
export interface LxcResource extends ComputeResourceBase<"lxc"> {
|
||||
vmId: number;
|
||||
}
|
||||
|
||||
export interface QemuResource extends ComputeResourceBase<"qemu"> {
|
||||
vmId: number;
|
||||
}
|
||||
|
||||
export interface StorageResource extends ResourceBase<"storage"> {
|
||||
storagePlugin: string;
|
||||
used: number; // previously disk
|
||||
total: number; // previously maxDisk
|
||||
isShared: boolean; // previously storageShared
|
||||
}
|
||||
|
||||
export type ComputeResource = NodeResource | LxcResource | QemuResource;
|
||||
export type Resource = ComputeResource | StorageResource;
|
||||
|
||||
export interface ProxmoxClusterInfo {
|
||||
nodes: NodeResource[];
|
||||
lxcs: LxcResource[];
|
||||
vms: QemuResource[];
|
||||
storages: StorageResource[];
|
||||
}
|
||||
@@ -6,4 +6,3 @@ export * from "./interfaces/media-requests/media-request";
|
||||
export * from "./pi-hole/pi-hole-types";
|
||||
export * from "./base/searchable-integration";
|
||||
export * from "./homeassistant/homeassistant-types";
|
||||
export * from "./proxmox/proxmox-types";
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Response } from "undici";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { IntegrationTestConnectionError } from "../src";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { objectEntries } from "@homarr/common";
|
||||
import type { Database, HomarrDatabaseMysql, InferInsertModel } from "@homarr/db";
|
||||
import type { Database, InferInsertModel } from "@homarr/db";
|
||||
import * as schema from "@homarr/db/schema";
|
||||
|
||||
type TableKey = {
|
||||
@@ -29,15 +29,5 @@ export const createDbInsertCollection = <TTableKey extends TableKey>(tablesInIns
|
||||
}
|
||||
});
|
||||
},
|
||||
insertAllAsync: async (db: HomarrDatabaseMysql) => {
|
||||
await db.transaction(async (transaction) => {
|
||||
for (const [key, values] of objectEntries(context)) {
|
||||
if (values.length >= 1) {
|
||||
// Below is actually the mysqlSchema when the driver is mysql
|
||||
await transaction.insert(schema[key] as never).values(values as never);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { z } from "zod";
|
||||
|
||||
import { Stopwatch } from "@homarr/common";
|
||||
import { handleTransactionsAsync } from "@homarr/db";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
@@ -35,21 +34,11 @@ export const importInitialOldmarrAsync = async (
|
||||
|
||||
logger.info("Inserting import data to database");
|
||||
|
||||
await handleTransactionsAsync(db, {
|
||||
async handleAsync(db) {
|
||||
await db.transaction(async (transaction) => {
|
||||
await boardInsertCollection.insertAllAsync(transaction);
|
||||
await userInsertCollection.insertAllAsync(transaction);
|
||||
await integrationInsertCollection.insertAllAsync(transaction);
|
||||
});
|
||||
},
|
||||
handleSync(db) {
|
||||
db.transaction((transaction) => {
|
||||
boardInsertCollection.insertAll(transaction);
|
||||
userInsertCollection.insertAll(transaction);
|
||||
integrationInsertCollection.insertAll(transaction);
|
||||
});
|
||||
},
|
||||
// Due to a limitation with better-sqlite it's only possible to use it synchronously
|
||||
db.transaction((transaction) => {
|
||||
boardInsertCollection.insertAll(transaction);
|
||||
userInsertCollection.insertAll(transaction);
|
||||
integrationInsertCollection.insertAll(transaction);
|
||||
});
|
||||
|
||||
logger.info(`Import successful (in ${stopwatch.getElapsedInHumanWords()})`);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { handleTransactionsAsync, inArray } from "@homarr/db";
|
||||
import { inArray } from "@homarr/db";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { apps } from "@homarr/db/schema";
|
||||
import type { OldmarrConfig } from "@homarr/old-schema";
|
||||
@@ -31,12 +31,6 @@ export const importSingleOldmarrConfigAsync = async (
|
||||
|
||||
const boardInsertCollection = createBoardInsertCollection({ preparedApps, preparedBoards }, settings);
|
||||
|
||||
await handleTransactionsAsync(db, {
|
||||
async handleAsync(db) {
|
||||
await boardInsertCollection.insertAllAsync(db);
|
||||
},
|
||||
handleSync(db) {
|
||||
boardInsertCollection.insertAll(db);
|
||||
},
|
||||
});
|
||||
// Due to a limitation with better-sqlite it's only possible to use it synchronously
|
||||
boardInsertCollection.insertAll(db);
|
||||
};
|
||||
@@ -141,9 +141,6 @@ const optionMapping: OptionMapping = {
|
||||
"fileSystem" in oldOptions
|
||||
? oldOptions.fileSystem
|
||||
: oldOptions.graphsOrder.some((graph) => graph.key === "storage" && graph.subValues.enabled),
|
||||
defaultTab: (oldOptions) => ("defaultTabState" in oldOptions ? oldOptions.defaultTabState : undefined),
|
||||
sectionIndicatorRequirement: (oldOptions) =>
|
||||
"sectionIndicatorColor" in oldOptions ? oldOptions.sectionIndicatorColor : undefined,
|
||||
},
|
||||
mediaTranscoding: {
|
||||
defaultView: (oldOptions) => oldOptions.defaultView,
|
||||
|
||||
@@ -2,13 +2,13 @@ import dayjs from "dayjs";
|
||||
|
||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||
import { integrationCreator } from "@homarr/integrations";
|
||||
import type { HealthMonitoring, ProxmoxClusterInfo } from "@homarr/integrations/types";
|
||||
import type { HealthMonitoring } from "@homarr/integrations/types";
|
||||
|
||||
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
||||
|
||||
export const systemInfoRequestHandler = createCachedIntegrationRequestHandler<
|
||||
HealthMonitoring,
|
||||
Exclude<IntegrationKindByCategory<"healthMonitoring">, "proxmox">,
|
||||
IntegrationKindByCategory<"healthMonitoring">,
|
||||
Record<string, never>
|
||||
>({
|
||||
async requestAsync(integration, _input) {
|
||||
@@ -18,16 +18,3 @@ export const systemInfoRequestHandler = createCachedIntegrationRequestHandler<
|
||||
cacheDuration: dayjs.duration(5, "seconds"),
|
||||
queryKey: "systemInfo",
|
||||
});
|
||||
|
||||
export const clusterInfoRequestHandler = createCachedIntegrationRequestHandler<
|
||||
ProxmoxClusterInfo,
|
||||
"proxmox",
|
||||
Record<string, never>
|
||||
>({
|
||||
async requestAsync(integration, _input) {
|
||||
const integrationInstance = integrationCreator(integration);
|
||||
return await integrationInstance.getClusterInfoAsync();
|
||||
},
|
||||
cacheDuration: dayjs.duration(5, "seconds"),
|
||||
queryKey: "clusterInfo",
|
||||
});
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "密码不符合要求",
|
||||
"boardAlreadyExists": "具有此名称的面板已存在",
|
||||
"invalidFileType": "无效的文件类型,例如 {expected}",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "文件太大,最大大小为 {maxSize}",
|
||||
"invalidConfiguration": "无效配置",
|
||||
"groupNameTaken": "用户组名称已存在"
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "Docker",
|
||||
"logs": "日志",
|
||||
"api": "API",
|
||||
"certificates": "",
|
||||
"tasks": "任务"
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": "此面板将显示为您的主面板"
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "设置为您的手机面板",
|
||||
"badge": {
|
||||
"label": "手机",
|
||||
"tooltip": "此面板将显示为您的手机面板"
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": "复制面板"
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "通用",
|
||||
"item": {
|
||||
"language": "语言与地区",
|
||||
"board": {
|
||||
"title": "主面板",
|
||||
"type": {
|
||||
"general": "通用",
|
||||
"mobile": "手机"
|
||||
}
|
||||
},
|
||||
"board": "主面板",
|
||||
"defaultSearchEngine": "默认搜索引擎",
|
||||
"firstDayOfWeek": "一周的第一天",
|
||||
"accessibility": "无障碍服务"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "面板",
|
||||
"homeBoard": {
|
||||
"label": "全局主面板",
|
||||
"mobileLabel": "全局手机面板",
|
||||
"description": "只有公共面板可供选择"
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": "日志"
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": "设置为主面板"
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": "设置为手机面板"
|
||||
},
|
||||
"settings": {
|
||||
"label": "打开设置"
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "Heslo nesplňuje požadavky",
|
||||
"boardAlreadyExists": "Plocha s tímto názvem již existuje",
|
||||
"invalidFileType": "Neplatný typ souboru, očekáván {expected}",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "Soubor je příliš velký, maximální velikost je {maxSize}",
|
||||
"invalidConfiguration": "Neplatná konfigurace",
|
||||
"groupNameTaken": "Název skupiny je již používán"
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "",
|
||||
"logs": "",
|
||||
"api": "",
|
||||
"certificates": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "Obecné",
|
||||
"item": {
|
||||
"language": "Jazyk & oblast",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "Domovská plocha",
|
||||
"defaultSearchEngine": "Výchozí vyhledávač",
|
||||
"firstDayOfWeek": "První den v týdnu",
|
||||
"accessibility": "Přístupnost"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "Plochy",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"mobileLabel": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": ""
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "",
|
||||
"boardAlreadyExists": "",
|
||||
"invalidFileType": "",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "",
|
||||
"invalidConfiguration": "",
|
||||
"groupNameTaken": ""
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "",
|
||||
"logs": "",
|
||||
"api": "",
|
||||
"certificates": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "Generelt",
|
||||
"item": {
|
||||
"language": "",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "",
|
||||
"defaultSearchEngine": "",
|
||||
"firstDayOfWeek": "Første ugedag",
|
||||
"accessibility": "Hjælpefunktioner"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"mobileLabel": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": ""
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,10 +213,10 @@
|
||||
"changeDefaultSearchEngine": {
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": "Standard-Suchmaschine erfolgreich geändert"
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"message": "Standard Suchmaschine kann nicht geändert werden"
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -499,7 +499,7 @@
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"search": "App suchen",
|
||||
"search": "",
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "Apps",
|
||||
@@ -625,8 +625,8 @@
|
||||
"label": "URL"
|
||||
},
|
||||
"attemptSearchEngineCreation": {
|
||||
"label": "Suchmaschinen anlegen",
|
||||
"description": "Integration \"{kind}\" kann mit den Suchmaschinen verwendet werden. Wählen Sie dies, um die Suchmaschine automatisch zu konfigurieren."
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "Passwort erfüllt nicht alle Anforderungen",
|
||||
"boardAlreadyExists": "Ein Board mit diesem Namen existiert bereits",
|
||||
"invalidFileType": "Ungültiger Dateityp, erwartete {expected}",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "Datei ist zu groß, maximale Größe ist {maxSize}",
|
||||
"invalidConfiguration": "Ungültige Konfiguration",
|
||||
"groupNameTaken": "Gruppenname bereits vergeben"
|
||||
@@ -919,7 +918,7 @@
|
||||
"category": {
|
||||
"field": {
|
||||
"name": {
|
||||
"label": "Name"
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
@@ -967,7 +966,7 @@
|
||||
},
|
||||
"create": {
|
||||
"title": "Wählen Sie das hinzuzufügende Element aus",
|
||||
"search": "Gegenstände filtern",
|
||||
"search": "",
|
||||
"addToBoard": "Zum Board hinzufügen"
|
||||
},
|
||||
"moveResize": {
|
||||
@@ -1009,7 +1008,7 @@
|
||||
},
|
||||
"widget": {
|
||||
"app": {
|
||||
"name": "App",
|
||||
"name": "",
|
||||
"description": "Bettet eine App in das Board ein.",
|
||||
"option": {
|
||||
"appId": {
|
||||
@@ -1046,24 +1045,24 @@
|
||||
"label": "Ansicht",
|
||||
"option": {
|
||||
"row": {
|
||||
"label": "Horizontal"
|
||||
"label": ""
|
||||
},
|
||||
"column": {
|
||||
"label": "Vertikal"
|
||||
},
|
||||
"grid": {
|
||||
"label": "Raster"
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"label": "Lesezeichen",
|
||||
"add": "Lesezeichen hinzufügen"
|
||||
"add": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"dnsHoleSummary": {
|
||||
"name": "DNS Hole Zusammenfassung",
|
||||
"name": "",
|
||||
"description": "Zeigt die Zusammenfassung deines DNS Holes an",
|
||||
"option": {
|
||||
"layout": {
|
||||
@@ -1094,7 +1093,7 @@
|
||||
"dnsQueriesToday": "Abfragen heute",
|
||||
"domainsBeingBlocked": "Domänen auf der Sperrliste"
|
||||
},
|
||||
"domainsTooltip": "Aufgrund mehrerer Integrationen kann Homarr die genaue Anzahl der blockierten Domains nicht berechnen"
|
||||
"domainsTooltip": ""
|
||||
},
|
||||
"dnsHoleControls": {
|
||||
"name": "DNS Hole Steuerung",
|
||||
@@ -1170,22 +1169,22 @@
|
||||
}
|
||||
},
|
||||
"minecraftServerStatus": {
|
||||
"name": "Status des Minecraft Servers",
|
||||
"description": "Status des Minecraft Servers anzeigen",
|
||||
"name": "",
|
||||
"description": "",
|
||||
"option": {
|
||||
"title": {
|
||||
"label": "Titel"
|
||||
"label": ""
|
||||
},
|
||||
"domain": {
|
||||
"label": "Server Adresse"
|
||||
"label": ""
|
||||
},
|
||||
"isBedrockServer": {
|
||||
"label": "Bedrock Server"
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"online": "Online",
|
||||
"offline": "Offline"
|
||||
"online": "",
|
||||
"offline": ""
|
||||
}
|
||||
},
|
||||
"notebook": {
|
||||
@@ -1283,7 +1282,7 @@
|
||||
},
|
||||
"error": {
|
||||
"noUrl": "Keine iFrame URL angegeben",
|
||||
"unsupportedProtocol": "Die angegebene URL verwendet ein nicht unterstütztes Protokoll. Bitte verwenden Sie eines von ({supportedProtocols})",
|
||||
"unsupportedProtocol": "",
|
||||
"noBrowerSupport": "Ihr Browser unterstützt keine iframes. Bitte aktualisieren Sie Ihren Browser."
|
||||
}
|
||||
},
|
||||
@@ -1418,37 +1417,37 @@
|
||||
"processor": "Prozessor: {cpuModelName}",
|
||||
"memory": "Speicher: {memory}GiB",
|
||||
"memoryAvailable": "Verfügbar: {memoryAvailable} GiB ({percent}%)",
|
||||
"version": "Version: {version}",
|
||||
"version": "",
|
||||
"uptime": "Laufzeit: {months} Monate, {days} Tage, {hours} Stunden, {minutes} Minuten",
|
||||
"loadAverage": "Durchschnittliche Last:",
|
||||
"minute": "1 Minute",
|
||||
"minutes": "{count} Minuten",
|
||||
"used": "Belegt",
|
||||
"available": "Verfügbar",
|
||||
"lastSeen": "Letzte Statusaktualisierung: {lastSeen}"
|
||||
"lastSeen": ""
|
||||
},
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": "Fehler beim Abrufen des Gesundheitsstatus"
|
||||
"internalServerError": ""
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"location": {
|
||||
"query": "Stadt / Postleitzahl",
|
||||
"latitude": "Breitengrad",
|
||||
"longitude": "Längengrad",
|
||||
"disabledTooltip": "Gib eine Stadt oder Postleitzahl an",
|
||||
"unknownLocation": "Unbekannter Ort",
|
||||
"query": "",
|
||||
"latitude": "",
|
||||
"longitude": "",
|
||||
"disabledTooltip": "",
|
||||
"unknownLocation": "",
|
||||
"search": "Suche",
|
||||
"table": {
|
||||
"header": {
|
||||
"city": "Stadt",
|
||||
"country": "Land",
|
||||
"coordinates": "Koordinaten",
|
||||
"population": "Bevölkerung"
|
||||
"city": "",
|
||||
"country": "",
|
||||
"coordinates": "",
|
||||
"population": ""
|
||||
},
|
||||
"action": {
|
||||
"select": "Wähle {city}, {countryCode}"
|
||||
"select": ""
|
||||
},
|
||||
"population": {
|
||||
"fallback": "Unbekannt"
|
||||
@@ -1456,16 +1455,16 @@
|
||||
}
|
||||
},
|
||||
"integration": {
|
||||
"noData": "Keine Integration gefunden",
|
||||
"description": "Klicken Sie <here></here> um eine neue Integration zu erstellen"
|
||||
"noData": "",
|
||||
"description": ""
|
||||
},
|
||||
"app": {
|
||||
"noData": "Keine App gefunden",
|
||||
"description": "Klicken Sie <here></here> um eine neue App zu erstellen"
|
||||
"description": ""
|
||||
},
|
||||
"error": {
|
||||
"noIntegration": "Keine Integration ausgewählt",
|
||||
"noData": "Keine Integrationsdaten verfügbar"
|
||||
"noIntegration": "",
|
||||
"noData": ""
|
||||
},
|
||||
"option": {}
|
||||
},
|
||||
@@ -1478,32 +1477,32 @@
|
||||
},
|
||||
"hasAutoPlay": {
|
||||
"label": "Automatische Wiedergabe",
|
||||
"description": "Autoplay funktioniert nur bei Stummschaltung aufgrund von Browser Einschränkungen"
|
||||
"description": ""
|
||||
},
|
||||
"isMuted": {
|
||||
"label": "Stumm geschaltet"
|
||||
"label": ""
|
||||
},
|
||||
"hasControls": {
|
||||
"label": "Steuerelemente anzeigen"
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"noUrl": "Keine Video URL angegeben",
|
||||
"forYoutubeUseIframe": "Für YouTube Videos verwenden Sie die iframe Option"
|
||||
"noUrl": "",
|
||||
"forYoutubeUseIframe": ""
|
||||
}
|
||||
},
|
||||
"mediaServer": {
|
||||
"name": "Aktuelle Media Server Streams",
|
||||
"description": "Zeige die aktuellen Streams auf deinen Medienservern an",
|
||||
"name": "",
|
||||
"description": "",
|
||||
"option": {},
|
||||
"items": {
|
||||
"user": "Benutzer",
|
||||
"name": "Name",
|
||||
"id": "Id"
|
||||
"user": "",
|
||||
"name": "",
|
||||
"id": ""
|
||||
}
|
||||
},
|
||||
"downloads": {
|
||||
"name": "Download Client",
|
||||
"name": "",
|
||||
"description": "",
|
||||
"option": {
|
||||
"columns": {
|
||||
@@ -1635,15 +1634,15 @@
|
||||
"delete": {
|
||||
"title": "",
|
||||
"modalTitle": "",
|
||||
"entry": "Eintrag löschen",
|
||||
"entryAndFiles": "Eintrag und Datei(en) löschen"
|
||||
"entry": "",
|
||||
"entryAndFiles": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"globalRatio": "Globales Verhältnis"
|
||||
"globalRatio": ""
|
||||
},
|
||||
"mediaRequests-requestList": {
|
||||
"name": "Liste der Medienanfragen",
|
||||
"name": "",
|
||||
"description": "Sehen Sie eine Liste aller Medienanfragen von Ihrer Overseerr- oder Jellyseerr-Instanz",
|
||||
"option": {
|
||||
"linksTargetNewTab": {
|
||||
@@ -1651,21 +1650,21 @@
|
||||
}
|
||||
},
|
||||
"pending": {
|
||||
"approve": "Anfrage annehmen",
|
||||
"approving": "Anfrage wird bestätigt...",
|
||||
"decline": "Antrag ablehnen"
|
||||
"approve": "",
|
||||
"approving": "",
|
||||
"decline": ""
|
||||
},
|
||||
"availability": {
|
||||
"unknown": "Unbekannt",
|
||||
"pending": "Ausstehend",
|
||||
"processing": "In Bearbeitung",
|
||||
"pending": "",
|
||||
"processing": "",
|
||||
"partiallyAvailable": "Teilweise",
|
||||
"available": "Verfügbar"
|
||||
},
|
||||
"toBeDetermined": "Noch Festzulegen"
|
||||
"toBeDetermined": ""
|
||||
},
|
||||
"mediaRequests-requestStats": {
|
||||
"name": "Statistik der Medienanfragen",
|
||||
"name": "",
|
||||
"description": "Statistiken über Ihre Medienanfragen",
|
||||
"option": {},
|
||||
"titles": {
|
||||
@@ -1673,38 +1672,38 @@
|
||||
"main": "Medien Statistiken",
|
||||
"approved": "Bereits genehmigt",
|
||||
"pending": "Ausstehende Freigaben",
|
||||
"processing": "Wird verarbeitet",
|
||||
"declined": "Bereits abgelehnt",
|
||||
"available": "Bereits verfügbar",
|
||||
"processing": "",
|
||||
"declined": "",
|
||||
"available": "",
|
||||
"tv": "TV Anfragen",
|
||||
"movie": "Film Anfragen",
|
||||
"total": "Gesamt"
|
||||
},
|
||||
"users": {
|
||||
"main": "Top Nutzer",
|
||||
"requests": "Anfragen"
|
||||
"requests": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"mediaTranscoding": {
|
||||
"name": "Medien Transkodierung",
|
||||
"description": "Statistiken, aktuelle Warteschlange und Status Ihrer Medien Transkordierung",
|
||||
"name": "",
|
||||
"description": "",
|
||||
"option": {
|
||||
"defaultView": {
|
||||
"label": "Standardansicht"
|
||||
},
|
||||
"queuePageSize": {
|
||||
"label": "Größe der Warteschlange"
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"tab": {
|
||||
"workers": "Arbeiter",
|
||||
"workers": "",
|
||||
"queue": "Warteschlange",
|
||||
"statistics": "Statistiken"
|
||||
},
|
||||
"currentIndex": "{start}-{end} von {total}",
|
||||
"healthCheck": {
|
||||
"title": "Gesundheitscheck",
|
||||
"title": "",
|
||||
"queued": "In der Warteschlange",
|
||||
"status": {
|
||||
"healthy": "Gesund",
|
||||
@@ -1715,12 +1714,12 @@
|
||||
"statistics": {
|
||||
"empty": "Leer",
|
||||
"transcodes": "Transkodieren",
|
||||
"transcodesCount": "Transkodierung: {value}",
|
||||
"healthChecksCount": "Gesundheitscheck: {value}",
|
||||
"filesCount": "Dateien: {value}",
|
||||
"savedSpace": "Gespeicherter Speicherplatz: {value}",
|
||||
"healthChecks": "Gesundheitscheck",
|
||||
"videoCodecs": "Codecs",
|
||||
"transcodesCount": "",
|
||||
"healthChecksCount": "",
|
||||
"filesCount": "",
|
||||
"savedSpace": "",
|
||||
"healthChecks": "",
|
||||
"videoCodecs": "",
|
||||
"videoContainers": "Container",
|
||||
"videoResolutions": "Auflösungen"
|
||||
},
|
||||
@@ -1731,7 +1730,7 @@
|
||||
"eta": "Voraussichtlicher Abschluss",
|
||||
"progress": "Fortschritt",
|
||||
"transcode": "Transkodieren",
|
||||
"healthCheck": "Gesundheitscheck"
|
||||
"healthCheck": ""
|
||||
}
|
||||
},
|
||||
"queue": {
|
||||
@@ -1740,52 +1739,52 @@
|
||||
"file": "Datei",
|
||||
"size": "Größe",
|
||||
"transcode": "Transkodieren",
|
||||
"healthCheck": "Gesundheitscheck"
|
||||
"healthCheck": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"rssFeed": {
|
||||
"name": "RSS Feeds",
|
||||
"description": "Einen oder mehrere generische RSS, ATOM oder JSON-Feeds überwachen und anzeigen",
|
||||
"name": "",
|
||||
"description": "",
|
||||
"option": {
|
||||
"feedUrls": {
|
||||
"label": "Feed URLs"
|
||||
"label": ""
|
||||
},
|
||||
"enableRtl": {
|
||||
"label": "RTL aktivieren"
|
||||
"label": ""
|
||||
},
|
||||
"textLinesClamp": {
|
||||
"label": "Beschreibung line clamp"
|
||||
"label": ""
|
||||
},
|
||||
"maximumAmountPosts": {
|
||||
"label": "Limit für Beiträge"
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"widgetPreview": {
|
||||
"toggle": {
|
||||
"enabled": "Bearbeitungsmodus aktiviert",
|
||||
"disabled": "Bearbeitungsmodus deaktiviert"
|
||||
"enabled": "",
|
||||
"disabled": ""
|
||||
},
|
||||
"dimensions": {
|
||||
"title": "Dimensionen ändern"
|
||||
"title": ""
|
||||
}
|
||||
},
|
||||
"board": {
|
||||
"action": {
|
||||
"duplicate": {
|
||||
"title": "Board duplizieren",
|
||||
"message": "Dies wird das Board {name} mit all seinen Inhalten duplizieren. Wenn Widgets Integrationen, hinterlegt sind die Sie nicht verwenden dürfen, werden diese entfernt.",
|
||||
"title": "",
|
||||
"message": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "Board dupliziert",
|
||||
"message": "Das Board wurde erfolgreich dupliziert"
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "Board konnte nicht dupliziert werden",
|
||||
"message": "Das Board konnte nicht dupliziert werden"
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1793,24 +1792,24 @@
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "Änderungen erfolgreich angewendet",
|
||||
"message": "Das Board wurde erfolgreich dupliziert"
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "Änderungen konnten nicht angewendet werden",
|
||||
"message": "Das Board konnte nicht gespeichert werden"
|
||||
"message": ""
|
||||
}
|
||||
},
|
||||
"confirmLeave": {
|
||||
"title": "Nicht gespeicherte Änderungen",
|
||||
"message": "Sie haben ungespeicherte Änderungen, sind sie sicher dass sie abbrechen möchten?"
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
},
|
||||
"oldImport": {
|
||||
"label": "Import von Homarr vor Version 1.0.0",
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "Import erfolgreich",
|
||||
"message": "Das Board wurde erfolgreich importiert"
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
@@ -1970,23 +1969,23 @@
|
||||
"title": "Hintergrund"
|
||||
},
|
||||
"color": {
|
||||
"title": "Farben"
|
||||
"title": ""
|
||||
},
|
||||
"customCss": {
|
||||
"title": "Benutzerdefiniertes CSS"
|
||||
"title": ""
|
||||
},
|
||||
"access": {
|
||||
"title": "Zugriffskontrolle",
|
||||
"title": "",
|
||||
"permission": {
|
||||
"item": {
|
||||
"view": {
|
||||
"label": "Board anzeigen"
|
||||
},
|
||||
"modify": {
|
||||
"label": "Board bearbeiten"
|
||||
"label": ""
|
||||
},
|
||||
"full": {
|
||||
"label": "Voller Zugriff"
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1995,41 +1994,41 @@
|
||||
"title": "Gefahrenbereich",
|
||||
"action": {
|
||||
"rename": {
|
||||
"label": "Board umbenennen",
|
||||
"description": "Das Ändern des Namens wird alle Links zu diesem Board aufheben.",
|
||||
"button": "Namen ändern",
|
||||
"label": "",
|
||||
"description": "",
|
||||
"button": "",
|
||||
"modal": {
|
||||
"title": "Board umbenennen"
|
||||
"title": ""
|
||||
}
|
||||
},
|
||||
"visibility": {
|
||||
"label": "Sichtbarkeit des Boards ändern",
|
||||
"label": "",
|
||||
"description": {
|
||||
"public": "Dieses Board ist derzeit öffentlich.",
|
||||
"private": "Dieses Board ist derzeit privat."
|
||||
"public": "",
|
||||
"private": ""
|
||||
},
|
||||
"button": {
|
||||
"public": "Als privat festlegen",
|
||||
"private": "Als öffentlich festlegen"
|
||||
"public": "",
|
||||
"private": ""
|
||||
},
|
||||
"confirm": {
|
||||
"public": {
|
||||
"title": "Board auf privat schalten",
|
||||
"description": "Sind Sie sicher, dass Sie dieses Board privat machen möchten? Dies wird das Board vor der Öffentlichkeit ausblenden. Links für Gastbenutzer werden unbrauchbar."
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"private": {
|
||||
"title": "Board auf öffentlich schalten",
|
||||
"description": "Sind Sie sicher, dass Sie dieses Board öffentlich machen möchten? Dies wird das Board für alle zugänglich machen."
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"label": "Board löschen",
|
||||
"description": "Sobald Sie ein Board löschen, gibt es kein zurück mehr. Bitte seien sie sich dessen bewusst.",
|
||||
"button": "Board löschen",
|
||||
"label": "",
|
||||
"description": "",
|
||||
"button": "",
|
||||
"confirm": {
|
||||
"title": "Board löschen",
|
||||
"description": "Sind Sie sicher, dass Sie dieses Board löschen möchten? Dies wird das Board und seinen gesamten Inhalt permanent löschen."
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2039,32 +2038,32 @@
|
||||
"error": {
|
||||
"noBoard": {
|
||||
"title": "Willkommen bei Homarr",
|
||||
"description": "Ein schlankens, modernes Dashboard, das alle Ihre Apps und Dienste bereit stellt.",
|
||||
"description": "",
|
||||
"link": "Erstellen Sie Ihr erstes Board",
|
||||
"notice": "Um diese Seite verschwinden zu lassen, erstellen Sie ein Board und setzen es als Home Board"
|
||||
"notice": ""
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Board nicht gefunden",
|
||||
"description": "Das angegebene Board wurde entweder nicht gefunden oder Sie haben keinen Zugriff darauf.",
|
||||
"title": "",
|
||||
"description": "",
|
||||
"link": "Alle Boards anzeigen",
|
||||
"notice": "Überprüfen Sie den Link oder wenden Sie sich an einen Administrator, wenn Sie glauben, dass er zugänglich sein sollte"
|
||||
"notice": ""
|
||||
},
|
||||
"homeBoard": {
|
||||
"title": "Kein Home Board",
|
||||
"title": "",
|
||||
"admin": {
|
||||
"description": "Es wurde noch kein Home Board für den Server festgelegt.",
|
||||
"link": "Serverweites Home Board konfigurieren",
|
||||
"notice": "Um diese Seite für alle Benutzer verschwinden zu lassen, legen sie ein Home-Board für den Server fest"
|
||||
"description": "",
|
||||
"link": "",
|
||||
"notice": ""
|
||||
},
|
||||
"user": {
|
||||
"description": "Es wurde noch kein Home Board festgelegt.",
|
||||
"link": "Home Board konfigurieren",
|
||||
"notice": "Um diese Seite verschwinden zu lassen, geben Sie das Home Board in Ihren Einstellungen ein"
|
||||
"description": "",
|
||||
"link": "",
|
||||
"notice": ""
|
||||
},
|
||||
"anonymous": {
|
||||
"description": "Der Server-Administrator hat noch kein Home Board eingerichtet.",
|
||||
"link": "Öffentliche Boards anzeigen",
|
||||
"notice": "Um diese Seite verschwinden zu lassen, bitten sie den Server Administrator, ein Home Board für den Server festzulegen"
|
||||
"description": "",
|
||||
"link": "",
|
||||
"notice": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2072,12 +2071,12 @@
|
||||
"management": {
|
||||
"metaTitle": "Verwaltung",
|
||||
"title": {
|
||||
"morning": "Guten Morgen, {username}",
|
||||
"afternoon": "Guten Nachmittag, {username}",
|
||||
"evening": "Guten Abend, {username}"
|
||||
"morning": "",
|
||||
"afternoon": "",
|
||||
"evening": ""
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Nicht gefunden",
|
||||
"title": "",
|
||||
"text": ""
|
||||
},
|
||||
"navbar": {
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "",
|
||||
"logs": "",
|
||||
"api": "",
|
||||
"certificates": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "Allgemein",
|
||||
"item": {
|
||||
"language": "",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "",
|
||||
"defaultSearchEngine": "",
|
||||
"firstDayOfWeek": "Erster Tag der Woche",
|
||||
"accessibility": "Barrierefreiheit"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"mobileLabel": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": ""
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "",
|
||||
"boardAlreadyExists": "",
|
||||
"invalidFileType": "",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "",
|
||||
"invalidConfiguration": "",
|
||||
"groupNameTaken": ""
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "",
|
||||
"logs": "",
|
||||
"api": "",
|
||||
"certificates": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "Γενικά",
|
||||
"item": {
|
||||
"language": "",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "",
|
||||
"defaultSearchEngine": "",
|
||||
"firstDayOfWeek": "Πρώτη ημέρα της εβδομάδας",
|
||||
"accessibility": "Προσβασιμότητα"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "Πίνακες",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"mobileLabel": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": ""
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -733,14 +733,6 @@
|
||||
"password": {
|
||||
"label": "Password",
|
||||
"newLabel": "New password"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "Token ID",
|
||||
"newLabel": "New token ID"
|
||||
},
|
||||
"realm": {
|
||||
"label": "Realm",
|
||||
"newLabel": "New realm"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -905,7 +897,6 @@
|
||||
"passwordRequirements": "Password does not meet the requirements",
|
||||
"boardAlreadyExists": "A board with this name already exists",
|
||||
"invalidFileType": "Invalid file type, expected {expected}",
|
||||
"invalidFileName": "Invalid file name",
|
||||
"fileTooLarge": "File is too large, maximum size is {maxSize}",
|
||||
"invalidConfiguration": "Invalid configuration",
|
||||
"groupNameTaken": "Group name already taken"
|
||||
@@ -1419,12 +1410,6 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": "Show Filesystem Info"
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": "Default tab"
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": "Section indicator requirement"
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1444,52 +1429,6 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": "Failed to fetch health status"
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "CPU",
|
||||
"memory": "RAM"
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": "Nodes"
|
||||
},
|
||||
"qemu": {
|
||||
"name": "VMs"
|
||||
},
|
||||
"lxc": {
|
||||
"name": "LXCs"
|
||||
},
|
||||
"storage": {
|
||||
"name": "Storage"
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "Node",
|
||||
"vmId": "VM ID",
|
||||
"plugin": "Plugin"
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "Cores",
|
||||
"memory": "Memory",
|
||||
"storage": "Storage",
|
||||
"uptime": "Uptime",
|
||||
"haState": "HA State",
|
||||
"storageType": {
|
||||
"local": "Local storage",
|
||||
"shared": "Shared storage"
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "Name",
|
||||
"cpu": "CPU",
|
||||
"memory": "RAM",
|
||||
"node": "Node"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
@@ -2162,7 +2101,6 @@
|
||||
"docker": "Docker",
|
||||
"logs": "Logs",
|
||||
"api": "API",
|
||||
"certificates": "Certificates",
|
||||
"tasks": "Tasks"
|
||||
}
|
||||
},
|
||||
@@ -2768,9 +2706,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": "Logs"
|
||||
},
|
||||
"certificates": {
|
||||
"label": "Certificates"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -3166,46 +3101,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "Trusted certificates",
|
||||
"description": "Used by Homarr to request data from integrations.",
|
||||
"noResults": {
|
||||
"title": "There are no certificates yet"
|
||||
},
|
||||
"expires": "Expires {when}"
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "Add certificate",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "Certificate added",
|
||||
"message": "The certificate was added successfully"
|
||||
},
|
||||
"error": {
|
||||
"title": "Failed to add certificate",
|
||||
"message": "The certificate could not be added"
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "Remove certificate",
|
||||
"confirm": "Are you sure you want to remove the certificate?",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "Certificate removed",
|
||||
"message": "The certificate was removed successfully"
|
||||
},
|
||||
"error": {
|
||||
"title": "Certificate not removed",
|
||||
"message": "The certificate could not be removed"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "",
|
||||
"boardAlreadyExists": "",
|
||||
"invalidFileType": "",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "",
|
||||
"invalidConfiguration": "",
|
||||
"groupNameTaken": ""
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "",
|
||||
"logs": "",
|
||||
"api": "",
|
||||
"certificates": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "",
|
||||
"item": {
|
||||
"language": "",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "",
|
||||
"defaultSearchEngine": "",
|
||||
"firstDayOfWeek": "Primer día de la semana",
|
||||
"accessibility": "Accesibilidad"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "Tableros",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"mobileLabel": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": ""
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "",
|
||||
"boardAlreadyExists": "",
|
||||
"invalidFileType": "",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "",
|
||||
"invalidConfiguration": "",
|
||||
"groupNameTaken": ""
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "",
|
||||
"logs": "",
|
||||
"api": "",
|
||||
"certificates": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "",
|
||||
"item": {
|
||||
"language": "",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "",
|
||||
"defaultSearchEngine": "",
|
||||
"firstDayOfWeek": "",
|
||||
"accessibility": ""
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"mobileLabel": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": ""
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "",
|
||||
"boardAlreadyExists": "",
|
||||
"invalidFileType": "",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "",
|
||||
"invalidConfiguration": "",
|
||||
"groupNameTaken": ""
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "",
|
||||
"logs": "",
|
||||
"api": "",
|
||||
"certificates": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "Général",
|
||||
"item": {
|
||||
"language": "",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "",
|
||||
"defaultSearchEngine": "",
|
||||
"firstDayOfWeek": "Premier jour de la semaine",
|
||||
"accessibility": "Accessibilité"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "Tableaux de bord",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"mobileLabel": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": ""
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "הסיסמה אינה עומדת בדרישות",
|
||||
"boardAlreadyExists": "לוח בשם זה כבר קיים",
|
||||
"invalidFileType": "סוג קובץ לא חוקי, צפוי {expected}",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "הקובץ גדול מדי, הגודל המרבי הוא {maxSize}",
|
||||
"invalidConfiguration": "תצורה לא חוקית",
|
||||
"groupNameTaken": "שם הקבוצה כבר תפוס"
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "דוקר",
|
||||
"logs": "קובצי לוג",
|
||||
"api": "ממשק API",
|
||||
"certificates": "",
|
||||
"tasks": "משימות"
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": "הלוח הזה יוצג כלוח הבית שלך"
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": "שכפול לוח"
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "כללי",
|
||||
"item": {
|
||||
"language": "שפה ואיזור",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "לוח בית",
|
||||
"defaultSearchEngine": "מנוע חיפוש ברירת מחדל",
|
||||
"firstDayOfWeek": "היום הראשון בשבוע",
|
||||
"accessibility": "נגישות"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "לוחות",
|
||||
"homeBoard": {
|
||||
"label": "לוח בית גלובלי",
|
||||
"mobileLabel": "",
|
||||
"description": "רק לוחות ציבוריים זמינים לבחירה"
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": "קבצי לוג"
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": "קביעה כלוח הבית"
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": "פתיחת הגדרות"
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "",
|
||||
"boardAlreadyExists": "",
|
||||
"invalidFileType": "",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "",
|
||||
"invalidConfiguration": "",
|
||||
"groupNameTaken": ""
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "",
|
||||
"logs": "",
|
||||
"api": "",
|
||||
"certificates": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "Općenito",
|
||||
"item": {
|
||||
"language": "",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "",
|
||||
"defaultSearchEngine": "",
|
||||
"firstDayOfWeek": "Prvi dan u tjednu",
|
||||
"accessibility": "Pristupačnost"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "Daske",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"mobileLabel": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": ""
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "",
|
||||
"boardAlreadyExists": "",
|
||||
"invalidFileType": "",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "",
|
||||
"invalidConfiguration": "",
|
||||
"groupNameTaken": ""
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "",
|
||||
"logs": "",
|
||||
"api": "",
|
||||
"certificates": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "Általános",
|
||||
"item": {
|
||||
"language": "",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "Főoldal",
|
||||
"defaultSearchEngine": "",
|
||||
"firstDayOfWeek": "A hét első napja",
|
||||
"accessibility": "Kisegítő lehetőségek"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "Táblák",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"mobileLabel": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": ""
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "",
|
||||
"boardAlreadyExists": "",
|
||||
"invalidFileType": "",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "",
|
||||
"invalidConfiguration": "",
|
||||
"groupNameTaken": ""
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "",
|
||||
"logs": "",
|
||||
"api": "",
|
||||
"certificates": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "Generale",
|
||||
"item": {
|
||||
"language": "",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "",
|
||||
"defaultSearchEngine": "",
|
||||
"firstDayOfWeek": "Primo giorno della settimana",
|
||||
"accessibility": "Accessibilità"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"mobileLabel": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": ""
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "",
|
||||
"boardAlreadyExists": "",
|
||||
"invalidFileType": "",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "",
|
||||
"invalidConfiguration": "",
|
||||
"groupNameTaken": ""
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "",
|
||||
"logs": "",
|
||||
"api": "",
|
||||
"certificates": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "一般",
|
||||
"item": {
|
||||
"language": "",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "",
|
||||
"defaultSearchEngine": "",
|
||||
"firstDayOfWeek": "週の初日",
|
||||
"accessibility": "アクセシビリティ"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "ボード",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"mobileLabel": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": ""
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "",
|
||||
"boardAlreadyExists": "",
|
||||
"invalidFileType": "",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "",
|
||||
"invalidConfiguration": "",
|
||||
"groupNameTaken": ""
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "",
|
||||
"logs": "",
|
||||
"api": "",
|
||||
"certificates": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "일반",
|
||||
"item": {
|
||||
"language": "",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "",
|
||||
"defaultSearchEngine": "",
|
||||
"firstDayOfWeek": "요일별 요일",
|
||||
"accessibility": "접근성"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "보드",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"mobileLabel": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": ""
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "",
|
||||
"boardAlreadyExists": "",
|
||||
"invalidFileType": "",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "",
|
||||
"invalidConfiguration": "",
|
||||
"groupNameTaken": ""
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "",
|
||||
"logs": "",
|
||||
"api": "",
|
||||
"certificates": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "Bendras",
|
||||
"item": {
|
||||
"language": "",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "",
|
||||
"defaultSearchEngine": "",
|
||||
"firstDayOfWeek": "",
|
||||
"accessibility": ""
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "Lentos",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"mobileLabel": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": ""
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "",
|
||||
"boardAlreadyExists": "",
|
||||
"invalidFileType": "",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "",
|
||||
"invalidConfiguration": "",
|
||||
"groupNameTaken": ""
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "",
|
||||
"logs": "",
|
||||
"api": "",
|
||||
"certificates": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "Vispārīgi",
|
||||
"item": {
|
||||
"language": "",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "",
|
||||
"defaultSearchEngine": "",
|
||||
"firstDayOfWeek": "Nedēļas pirmā diena",
|
||||
"accessibility": "Piekļūstamība"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "Dēļi",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"mobileLabel": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": ""
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "Wachtwoord voldoet niet aan de eisen",
|
||||
"boardAlreadyExists": "Er bestaat al een bord met deze naam",
|
||||
"invalidFileType": "Ongeldig bestandstype, verwachtte {expected}",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "Bestand is te groot, maximale grootte is {maxSize}",
|
||||
"invalidConfiguration": "Ongeldige configuratie",
|
||||
"groupNameTaken": "Groepsnaam al in gebruik"
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "Docker",
|
||||
"logs": "Logs",
|
||||
"api": "API",
|
||||
"certificates": "",
|
||||
"tasks": "Taken"
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": "Dit bord wordt weergegeven als je home-bord"
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "Algemeen",
|
||||
"item": {
|
||||
"language": "Taal & regio",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "Home-bord",
|
||||
"defaultSearchEngine": "",
|
||||
"firstDayOfWeek": "Eerste dag van de week",
|
||||
"accessibility": "Toegankelijkheid"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "Borden",
|
||||
"homeBoard": {
|
||||
"label": "Globaal home-bord",
|
||||
"mobileLabel": "",
|
||||
"description": "Alleen openbare borden zijn beschikbaar voor selectie"
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": "Logs"
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": "Instellen als home-bord"
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": "Instellingen openen"
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "",
|
||||
"boardAlreadyExists": "",
|
||||
"invalidFileType": "",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "",
|
||||
"invalidConfiguration": "",
|
||||
"groupNameTaken": ""
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "",
|
||||
"logs": "",
|
||||
"api": "",
|
||||
"certificates": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "Generelt",
|
||||
"item": {
|
||||
"language": "",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "",
|
||||
"defaultSearchEngine": "",
|
||||
"firstDayOfWeek": "Første dag i uken",
|
||||
"accessibility": "Hjelpemidler"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "Tavler",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"mobileLabel": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": ""
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "Twoje hasło nie spełnia wymagań.",
|
||||
"boardAlreadyExists": "Tablica o tej nazwie już istnieje",
|
||||
"invalidFileType": "Nieprawidłowy typ pliku, oczekiwany {expected}",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "Plik jest zbyt duży, maksymalny rozmiar to {maxSize}",
|
||||
"invalidConfiguration": "Nieprawidłowa konfiguracja",
|
||||
"groupNameTaken": "Nazwa użytkownika jest już zajęta"
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "",
|
||||
"logs": "",
|
||||
"api": "",
|
||||
"certificates": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "Ogólne",
|
||||
"item": {
|
||||
"language": "",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "",
|
||||
"defaultSearchEngine": "",
|
||||
"firstDayOfWeek": "Pierwszy dzień tygodnia",
|
||||
"accessibility": "Ułatwienia dostępu"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "Tablice",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"mobileLabel": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": ""
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "",
|
||||
"boardAlreadyExists": "",
|
||||
"invalidFileType": "",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "",
|
||||
"invalidConfiguration": "",
|
||||
"groupNameTaken": ""
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "",
|
||||
"logs": "",
|
||||
"api": "",
|
||||
"certificates": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "Geral",
|
||||
"item": {
|
||||
"language": "",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "",
|
||||
"defaultSearchEngine": "",
|
||||
"firstDayOfWeek": "Primeiro dia da semana",
|
||||
"accessibility": "Acessibilidade"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "Placas",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"mobileLabel": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": ""
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "",
|
||||
"boardAlreadyExists": "",
|
||||
"invalidFileType": "",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "",
|
||||
"invalidConfiguration": "",
|
||||
"groupNameTaken": ""
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "",
|
||||
"logs": "",
|
||||
"api": "",
|
||||
"certificates": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "",
|
||||
"item": {
|
||||
"language": "",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "",
|
||||
"defaultSearchEngine": "",
|
||||
"firstDayOfWeek": "Prima zi a săptămâni",
|
||||
"accessibility": "Accesibilitate"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "Planșe",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"mobileLabel": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": ""
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "Пароль не соответствует требованиям",
|
||||
"boardAlreadyExists": "Панель с таким именем уже существует",
|
||||
"invalidFileType": "Неверный тип файла, ожидается {expected}",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "Файл слишком большой, максимальный размер {maxSize}",
|
||||
"invalidConfiguration": "Неверная конфигурация",
|
||||
"groupNameTaken": "Имя группы уже занято"
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "Docker",
|
||||
"logs": "Логи",
|
||||
"api": "API",
|
||||
"certificates": "",
|
||||
"tasks": "Задачи"
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": "Эта панель будет показываться как ваша домашняя панель"
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "Общие",
|
||||
"item": {
|
||||
"language": "Язык и регион",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "Домашняя панель",
|
||||
"defaultSearchEngine": "",
|
||||
"firstDayOfWeek": "Первый день недели",
|
||||
"accessibility": "Специальные возможности"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "Панели",
|
||||
"homeBoard": {
|
||||
"label": "Общая домашняя панель",
|
||||
"mobileLabel": "",
|
||||
"description": "Для выбора доступны только публичные панели"
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": "Логи"
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": "Сделать домашней панелью"
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": "Открыть настройки"
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "Heslo nespĺňa požiadavky",
|
||||
"boardAlreadyExists": "Doska s týmto názvom už existuje",
|
||||
"invalidFileType": "Neplatný typ súboru, očakáva sa {expected}",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "Súbor je príliš veľký, maximálna veľkosť je {maxSize}",
|
||||
"invalidConfiguration": "Neplatná konfigurácia",
|
||||
"groupNameTaken": "Názov skupiny je už obsadený"
|
||||
@@ -967,7 +966,7 @@
|
||||
},
|
||||
"create": {
|
||||
"title": "Vyberte položku, ktorú chcete pridať",
|
||||
"search": "Filtrovať položky",
|
||||
"search": "",
|
||||
"addToBoard": "Pridať na dosku"
|
||||
},
|
||||
"moveResize": {
|
||||
@@ -1094,7 +1093,7 @@
|
||||
"dnsQueriesToday": "Poziadavky dnes",
|
||||
"domainsBeingBlocked": "Domény na adlistoch"
|
||||
},
|
||||
"domainsTooltip": "Kvôli viacerým integráciám nemôže Homarr vypočítať presný počet blokovaných domén"
|
||||
"domainsTooltip": ""
|
||||
},
|
||||
"dnsHoleControls": {
|
||||
"name": "Kontrola diery DNS",
|
||||
@@ -1283,7 +1282,7 @@
|
||||
},
|
||||
"error": {
|
||||
"noUrl": "Nie je uvedená žiadna adresa URL iFrame",
|
||||
"unsupportedProtocol": "Uvedená adresa URL používa nepodporovaný protokol. Použite jeden z ({supportedProtocols})",
|
||||
"unsupportedProtocol": "",
|
||||
"noBrowerSupport": "Váš prehliadač nepodporuje iframe. Aktualizujte svoj prehliadač."
|
||||
}
|
||||
},
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "Docker",
|
||||
"logs": "Záznamy",
|
||||
"api": "API",
|
||||
"certificates": "",
|
||||
"tasks": "Úlohy"
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": "Táto tabuľa sa zobrazí ako vaša domovská tabuľa"
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "Nastaviť ako mobilnú tabuľu",
|
||||
"badge": {
|
||||
"label": "Mobilný",
|
||||
"tooltip": "Táto tabuľa sa zobrazí ako vaša mobilná tabuľa"
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": "Duplikovať dosku"
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "Všeobecné",
|
||||
"item": {
|
||||
"language": "Jazyk a región",
|
||||
"board": {
|
||||
"title": "Domáca doska",
|
||||
"type": {
|
||||
"general": "Hlavné",
|
||||
"mobile": "Mobilná"
|
||||
}
|
||||
},
|
||||
"board": "Domáca doska",
|
||||
"defaultSearchEngine": "Predvolený vyhľadávací nástroj",
|
||||
"firstDayOfWeek": "Prvý deň v týždni",
|
||||
"accessibility": "Prístupnosť"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "Dosky",
|
||||
"homeBoard": {
|
||||
"label": "Globálna domovská nástenka",
|
||||
"mobileLabel": "Globálna mobilná doska",
|
||||
"description": "Na výber sú k dispozícii iba verejné tabule"
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": "Záznamy"
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": "Nastaviť ako domácu dosku"
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": "Nastaviť ako mobilnú tabuľu"
|
||||
},
|
||||
"settings": {
|
||||
"label": "Otvorte nastavenia"
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "",
|
||||
"boardAlreadyExists": "",
|
||||
"invalidFileType": "",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "",
|
||||
"invalidConfiguration": "",
|
||||
"groupNameTaken": ""
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "",
|
||||
"logs": "",
|
||||
"api": "",
|
||||
"certificates": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "Splošno",
|
||||
"item": {
|
||||
"language": "",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "",
|
||||
"defaultSearchEngine": "",
|
||||
"firstDayOfWeek": "Prvi dan v tednu",
|
||||
"accessibility": "Dostopnost"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "Deske",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"mobileLabel": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": ""
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "",
|
||||
"boardAlreadyExists": "",
|
||||
"invalidFileType": "",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "",
|
||||
"invalidConfiguration": "",
|
||||
"groupNameTaken": ""
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "",
|
||||
"logs": "",
|
||||
"api": "",
|
||||
"certificates": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "Allmänt",
|
||||
"item": {
|
||||
"language": "",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "",
|
||||
"defaultSearchEngine": "",
|
||||
"firstDayOfWeek": "Första veckodagen",
|
||||
"accessibility": "Tillgänglighet"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "Tavlor",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"mobileLabel": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": ""
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,13 +51,13 @@
|
||||
"title": "İçe Aktarma Token'ını Girin",
|
||||
"field": {
|
||||
"token": {
|
||||
"label": "Token",
|
||||
"label": "Erişim Anahtarı",
|
||||
"description": "Önceki Homarr Örneğinizden Aldığınız İçe Aktarma Token'ını girin"
|
||||
}
|
||||
},
|
||||
"notification": {
|
||||
"error": {
|
||||
"title": "Token",
|
||||
"title": "Geçersiz Token",
|
||||
"message": "Girdiğiniz Token Geçersiz"
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "Parola gereksinimleri karşılamıyor",
|
||||
"boardAlreadyExists": "Bu isimde bir panel zaten mevcut",
|
||||
"invalidFileType": "Geçersiz dosya türü, {expected} bekleniyor",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "Dosya çok büyük, azami boyut {maxSize}",
|
||||
"invalidConfiguration": "Geçersiz yapılandırma",
|
||||
"groupNameTaken": "Grup adı zaten alınmış"
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "Docker",
|
||||
"logs": "Günlükler",
|
||||
"api": "API",
|
||||
"certificates": "",
|
||||
"tasks": "Görevler"
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": "Bu panel sizin öntanımlı paneliniz olarak görüntülenecektir"
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "Mobil Panel Olarak Ata",
|
||||
"badge": {
|
||||
"label": "Mobil",
|
||||
"tooltip": "Bu panel sizin mobil paneliniz olarak görüntülenecektir"
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": "Paneli Kopyala"
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "Genel",
|
||||
"item": {
|
||||
"language": "Dil & Bölge",
|
||||
"board": {
|
||||
"title": "Ana Panel",
|
||||
"type": {
|
||||
"general": "Genel",
|
||||
"mobile": "Mobil"
|
||||
}
|
||||
},
|
||||
"board": "Öntanımlı panel",
|
||||
"defaultSearchEngine": "Öntanımlı arama motoru",
|
||||
"firstDayOfWeek": "Haftanın ilk günü",
|
||||
"accessibility": "Erişilebilirlik"
|
||||
@@ -2283,7 +2268,7 @@
|
||||
"label": "Son geçerlilik tarihi"
|
||||
},
|
||||
"token": {
|
||||
"label": "Token"
|
||||
"label": "Erişim Anahtarı"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "Paneller",
|
||||
"homeBoard": {
|
||||
"label": "Genel Ana Sayfa Paneli",
|
||||
"mobileLabel": "Genel Mobil Panel",
|
||||
"description": "Seçim yalnızca herkese açık paneller için kullanılabilir"
|
||||
}
|
||||
},
|
||||
@@ -2460,7 +2444,7 @@
|
||||
"modal": {
|
||||
"createApiToken": {
|
||||
"title": "API token'ı oluşturuldu",
|
||||
"description": "API anahtarı oluşturuldu. Dikkatli olun, bu anahtar veritabanında şifrelenmiştir ve size bir daha asla aktarılmayacaktır. Bu anaharı kaybederseniz, bir daha erişmeniz mümkün değildir.",
|
||||
"description": "API token'ı oluşturuldu. Dikkatli olun, bu token veritabanında şifrelenmiştir ve size bir daha asla aktarılmayacaktır. Bu belirteci kaybederseniz, bu token'ı artık geri alamazsınız.",
|
||||
"button": "Kopyala ve kapat"
|
||||
}
|
||||
},
|
||||
@@ -2613,7 +2597,7 @@
|
||||
}
|
||||
},
|
||||
"modal": {
|
||||
"title": "Docker Konteynerleri Homarr'a Ekle"
|
||||
"title": "Homarr'a docker konteyner(-ler)i ekleyin"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": "Günlükler"
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": "Öntanımlı panel olarak ata"
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": "Mobil Panel Olarak Ata"
|
||||
},
|
||||
"settings": {
|
||||
"label": "Ayarları aç"
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "",
|
||||
"boardAlreadyExists": "",
|
||||
"invalidFileType": "",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "",
|
||||
"invalidConfiguration": "",
|
||||
"groupNameTaken": ""
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "",
|
||||
"logs": "",
|
||||
"api": "",
|
||||
"certificates": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "Загальне",
|
||||
"item": {
|
||||
"language": "",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "",
|
||||
"defaultSearchEngine": "",
|
||||
"firstDayOfWeek": "Перший день тижня",
|
||||
"accessibility": "Доступність"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "Дошки",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"mobileLabel": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": ""
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "",
|
||||
"boardAlreadyExists": "",
|
||||
"invalidFileType": "",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "",
|
||||
"invalidConfiguration": "",
|
||||
"groupNameTaken": ""
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "",
|
||||
"logs": "",
|
||||
"api": "",
|
||||
"certificates": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "Chung",
|
||||
"item": {
|
||||
"language": "",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "",
|
||||
"defaultSearchEngine": "",
|
||||
"firstDayOfWeek": "Ngày đầu tiên trong tuần",
|
||||
"accessibility": "Trợ năng"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "Bảng",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"mobileLabel": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": ""
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +897,6 @@
|
||||
"passwordRequirements": "密碼不符合要求",
|
||||
"boardAlreadyExists": "此面板名稱已存在",
|
||||
"invalidFileType": "無效的檔案類型,例如 {expected}",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "檔案太大,最大大小為 {maxSize}",
|
||||
"invalidConfiguration": "無效設定",
|
||||
"groupNameTaken": "用戶組名稱已存在"
|
||||
@@ -2102,7 +2101,6 @@
|
||||
"docker": "Docker",
|
||||
"logs": "Logs",
|
||||
"api": "API",
|
||||
"certificates": "",
|
||||
"tasks": "任務"
|
||||
}
|
||||
},
|
||||
@@ -2155,13 +2153,6 @@
|
||||
"tooltip": "此面板將顯示為主面板"
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": "複製面板"
|
||||
},
|
||||
@@ -2198,13 +2189,7 @@
|
||||
"title": "一般",
|
||||
"item": {
|
||||
"language": "語言與地區",
|
||||
"board": {
|
||||
"title": "",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
}
|
||||
},
|
||||
"board": "主面板",
|
||||
"defaultSearchEngine": "預設搜尋引擎",
|
||||
"firstDayOfWeek": "一周的第一天",
|
||||
"accessibility": "無障礙服務"
|
||||
@@ -2364,7 +2349,6 @@
|
||||
"title": "面板",
|
||||
"homeBoard": {
|
||||
"label": "全局主面板",
|
||||
"mobileLabel": "",
|
||||
"description": "只有公開面板可供選擇"
|
||||
}
|
||||
},
|
||||
@@ -2708,9 +2692,6 @@
|
||||
},
|
||||
"logs": {
|
||||
"label": "Logs"
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2757,9 +2738,6 @@
|
||||
"homeBoard": {
|
||||
"label": "設定為主面板"
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": "開啟設定"
|
||||
}
|
||||
@@ -3106,46 +3084,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { createCustomErrorParams } from "./form/i18n";
|
||||
|
||||
const validFileNameSchema = z.string().regex(/^[\w\-. ]+$/);
|
||||
|
||||
export const superRefineCertificateFile = (value: File | null, context: z.RefinementCtx) => {
|
||||
if (!value) {
|
||||
return context.addIssue({
|
||||
code: "invalid_type",
|
||||
expected: "object",
|
||||
received: "null",
|
||||
});
|
||||
}
|
||||
|
||||
const result = validFileNameSchema.safeParse(value.name);
|
||||
if (!result.success) {
|
||||
return context.addIssue({
|
||||
code: "custom",
|
||||
params: createCustomErrorParams({
|
||||
key: "invalidFileName",
|
||||
params: {},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (value.type !== "application/x-x509-ca-cert" && value.type !== "application/pkix-cert") {
|
||||
return context.addIssue({
|
||||
code: "custom",
|
||||
params: createCustomErrorParams({
|
||||
key: "invalidFileType",
|
||||
params: { expected: ".crt" },
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (value.size > 1024 * 1024) {
|
||||
return context.addIssue({
|
||||
code: "custom",
|
||||
params: createCustomErrorParams({
|
||||
key: "fileTooLarge",
|
||||
params: { maxSize: "1 MB" },
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const certificateSchemas = {
|
||||
validFileNameSchema,
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import { appSchemas } from "./app";
|
||||
import { boardSchemas } from "./board";
|
||||
import { certificateSchemas } from "./certificates";
|
||||
import { commonSchemas } from "./common";
|
||||
import { groupSchemas } from "./group";
|
||||
import { iconsSchemas } from "./icons";
|
||||
@@ -25,7 +24,6 @@ export const validation = {
|
||||
media: mediaSchemas,
|
||||
settings: settingsSchemas,
|
||||
common: commonSchemas,
|
||||
certificates: certificateSchemas,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -35,7 +33,6 @@ export {
|
||||
type BoardItemAdvancedOptions,
|
||||
type BoardItemIntegration,
|
||||
} from "./shared";
|
||||
export { superRefineCertificateFile } from "./certificates";
|
||||
export { passwordRequirements } from "./user";
|
||||
export { supportedMediaUploadFormats } from "./media";
|
||||
export { zodEnumFromArray, zodUnionFromArray } from "./enums";
|
||||
@@ -1,187 +0,0 @@
|
||||
import { Accordion, Center, Flex, Group, RingProgress, Stack, Text } from "@mantine/core";
|
||||
import { IconBrain, IconCpu, IconCube, IconDatabase, IconDeviceLaptop, IconServer } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import type { Resource } from "@homarr/integrations/types";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetComponentProps } from "../../definition";
|
||||
import { formatUptime } from "../system-health";
|
||||
import { ResourceAccordionItem } from "./resource-accordion-item";
|
||||
import { ResourceTable } from "./resource-table";
|
||||
|
||||
const addBadgeColor = ({
|
||||
activeCount,
|
||||
totalCount,
|
||||
sectionIndicatorRequirement,
|
||||
}: {
|
||||
activeCount: number;
|
||||
totalCount: number;
|
||||
sectionIndicatorRequirement: WidgetComponentProps<"healthMonitoring">["options"]["sectionIndicatorRequirement"];
|
||||
}) => ({
|
||||
color: activeCount === totalCount || (sectionIndicatorRequirement === "any" && activeCount >= 1) ? "green" : "orange",
|
||||
activeCount,
|
||||
totalCount,
|
||||
});
|
||||
|
||||
const running = (total: number, current: Resource) => {
|
||||
return current.isRunning ? total + 1 : total;
|
||||
};
|
||||
|
||||
export const ClusterHealthMonitoring = ({
|
||||
integrationId,
|
||||
options,
|
||||
}: WidgetComponentProps<"healthMonitoring"> & { integrationId: string }) => {
|
||||
const t = useI18n();
|
||||
const [healthData] = clientApi.widget.healthMonitoring.getClusterHealthStatus.useSuspenseQuery(
|
||||
{
|
||||
integrationId,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
},
|
||||
);
|
||||
|
||||
const utils = clientApi.useUtils();
|
||||
clientApi.widget.healthMonitoring.subscribeClusterHealthStatus.useSubscription(
|
||||
{ integrationId },
|
||||
{
|
||||
onData(data) {
|
||||
utils.widget.healthMonitoring.getClusterHealthStatus.setData({ integrationId }, data);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const activeNodes = healthData.nodes.reduce(running, 0);
|
||||
const activeVMs = healthData.vms.reduce(running, 0);
|
||||
const activeLXCs = healthData.lxcs.reduce(running, 0);
|
||||
const activeStorage = healthData.storages.reduce(running, 0);
|
||||
|
||||
const usedMem = healthData.nodes.reduce((sum, item) => (item.isRunning ? item.memory.used + sum : sum), 0);
|
||||
const maxMem = healthData.nodes.reduce((sum, item) => (item.isRunning ? item.memory.total + sum : sum), 0);
|
||||
const maxCpu = healthData.nodes.reduce((sum, item) => (item.isRunning ? item.cpu.cores + sum : sum), 0);
|
||||
const usedCpu = healthData.nodes.reduce(
|
||||
(sum, item) => (item.isRunning ? item.cpu.utilization * item.cpu.cores + sum : sum),
|
||||
0,
|
||||
);
|
||||
const uptime = healthData.nodes.reduce((sum, { uptime }) => (sum > uptime ? sum : uptime), 0);
|
||||
|
||||
const cpuPercent = maxCpu ? (usedCpu / maxCpu) * 100 : 0;
|
||||
const memPercent = maxMem ? (usedMem / maxMem) * 100 : 0;
|
||||
|
||||
return (
|
||||
<Stack h="100%">
|
||||
<Group justify="center" wrap="nowrap" pt="md">
|
||||
<Text fz="md" tt="uppercase" fw={700} c="dimmed" ta="center">
|
||||
{formatUptime(uptime, t)}
|
||||
</Text>
|
||||
</Group>
|
||||
<SummaryHeader cpu={cpuPercent} memory={memPercent} />
|
||||
<Accordion variant="contained" chevronPosition="right" multiple defaultValue={["node"]}>
|
||||
<ResourceAccordionItem
|
||||
value="node"
|
||||
title={t("widget.healthMonitoring.cluster.resource.node.name")}
|
||||
icon={IconServer}
|
||||
badge={addBadgeColor({
|
||||
activeCount: activeNodes,
|
||||
totalCount: healthData.nodes.length,
|
||||
sectionIndicatorRequirement: options.sectionIndicatorRequirement,
|
||||
})}
|
||||
>
|
||||
<ResourceTable type="node" data={healthData.nodes} />
|
||||
</ResourceAccordionItem>
|
||||
|
||||
<ResourceAccordionItem
|
||||
value="qemu"
|
||||
title={t("widget.healthMonitoring.cluster.resource.qemu.name")}
|
||||
icon={IconDeviceLaptop}
|
||||
badge={addBadgeColor({
|
||||
activeCount: activeVMs,
|
||||
totalCount: healthData.vms.length,
|
||||
sectionIndicatorRequirement: options.sectionIndicatorRequirement,
|
||||
})}
|
||||
>
|
||||
<ResourceTable type="qemu" data={healthData.vms} />
|
||||
</ResourceAccordionItem>
|
||||
|
||||
<ResourceAccordionItem
|
||||
value="lxc"
|
||||
title={t("widget.healthMonitoring.cluster.resource.lxc.name")}
|
||||
icon={IconCube}
|
||||
badge={addBadgeColor({
|
||||
activeCount: activeLXCs,
|
||||
totalCount: healthData.lxcs.length,
|
||||
sectionIndicatorRequirement: options.sectionIndicatorRequirement,
|
||||
})}
|
||||
>
|
||||
<ResourceTable type="lxc" data={healthData.lxcs} />
|
||||
</ResourceAccordionItem>
|
||||
|
||||
<ResourceAccordionItem
|
||||
value="storage"
|
||||
title={t("widget.healthMonitoring.cluster.resource.storage.name")}
|
||||
icon={IconDatabase}
|
||||
badge={addBadgeColor({
|
||||
activeCount: activeStorage,
|
||||
totalCount: healthData.storages.length,
|
||||
sectionIndicatorRequirement: options.sectionIndicatorRequirement,
|
||||
})}
|
||||
>
|
||||
<ResourceTable type="storage" data={healthData.storages} />
|
||||
</ResourceAccordionItem>
|
||||
</Accordion>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
interface SummaryHeaderProps {
|
||||
cpu: number;
|
||||
memory: number;
|
||||
}
|
||||
|
||||
const SummaryHeader = ({ cpu, memory }: SummaryHeaderProps) => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<Center>
|
||||
<Group wrap="nowrap">
|
||||
<Flex direction="row">
|
||||
<RingProgress
|
||||
roundCaps
|
||||
size={60}
|
||||
thickness={6}
|
||||
label={
|
||||
<Center>
|
||||
<IconCpu />
|
||||
</Center>
|
||||
}
|
||||
sections={[{ value: cpu, color: cpu > 75 ? "orange" : "green" }]}
|
||||
/>
|
||||
<Stack align="center" justify="center" gap={0}>
|
||||
<Text fw={500}>{t("widget.healthMonitoring.cluster.summary.cpu")}</Text>
|
||||
<Text>{cpu.toFixed(1)}%</Text>
|
||||
</Stack>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<RingProgress
|
||||
roundCaps
|
||||
size={60}
|
||||
thickness={6}
|
||||
label={
|
||||
<Center>
|
||||
<IconBrain />
|
||||
</Center>
|
||||
}
|
||||
sections={[{ value: memory, color: memory > 75 ? "orange" : "green" }]}
|
||||
/>
|
||||
<Stack align="center" justify="center" gap={0}>
|
||||
<Text fw={500}>{t("widget.healthMonitoring.cluster.summary.memory")}</Text>
|
||||
<Text>{memory.toFixed(1)}%</Text>
|
||||
</Stack>
|
||||
</Flex>
|
||||
</Group>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
Loaded 100 of 110 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user