diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5479cfd41..5580d85bc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -31,6 +31,7 @@ body: label: Version description: What version of Homarr are you running? options: + - 1.30.1 - 1.30.0 - 1.29.0 - 1.28.1 diff --git a/.nvmrc b/.nvmrc index 7377d130e..91d5f6ff8 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.17.1 +22.18.0 diff --git a/Dockerfile b/Dockerfile index 5cc126cec..d40b0a3f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22.17.0-alpine AS base +FROM node:22.17.1-alpine AS base FROM base AS builder RUN apk add --no-cache libc6-compat diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index 69ba55992..d4c143b88 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -50,17 +50,17 @@ "@homarr/ui": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", "@homarr/widgets": "workspace:^0.1.0", - "@mantine/colors-generator": "^8.2.1", - "@mantine/core": "^8.2.1", - "@mantine/dropzone": "^8.2.1", - "@mantine/hooks": "^8.2.1", - "@mantine/modals": "^8.2.1", - "@mantine/tiptap": "^8.2.1", + "@mantine/colors-generator": "^8.2.2", + "@mantine/core": "^8.2.2", + "@mantine/dropzone": "^8.2.2", + "@mantine/hooks": "^8.2.2", + "@mantine/modals": "^8.2.2", + "@mantine/tiptap": "^8.2.2", "@million/lint": "1.0.14", "@tabler/icons-react": "^3.34.1", - "@tanstack/react-query": "^5.83.0", - "@tanstack/react-query-devtools": "^5.83.0", - "@tanstack/react-query-next-experimental": "^5.83.0", + "@tanstack/react-query": "^5.84.1", + "@tanstack/react-query-devtools": "^5.84.1", + "@tanstack/react-query-next-experimental": "^5.84.1", "@trpc/client": "^11.4.3", "@trpc/next": "^11.4.3", "@trpc/react-query": "^11.4.3", @@ -76,16 +76,16 @@ "glob": "^11.0.3", "jotai": "^2.12.5", "mantine-react-table": "2.0.0-beta.9", - "next": "15.4.4", + "next": "15.4.5", "postcss-preset-mantine": "^1.18.0", "prismjs": "^1.30.0", - "react": "19.1.0", - "react-dom": "19.1.0", + "react": "19.1.1", + "react-dom": "19.1.1", "react-error-boundary": "^6.0.0", "react-simple-code-editor": "^0.14.1", "sass": "^1.89.2", "superjson": "2.2.2", - "swagger-ui-react": "^5.27.0", + "swagger-ui-react": "^5.27.1", "use-deep-compare-effect": "^1.8.1", "zod": "^3.25.76" }, @@ -94,15 +94,15 @@ "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "@types/chroma-js": "3.1.1", - "@types/node": "^22.16.4", + "@types/node": "^22.17.0", "@types/prismjs": "^1.26.5", - "@types/react": "19.1.8", - "@types/react-dom": "19.1.6", + "@types/react": "19.1.9", + "@types/react-dom": "19.1.7", "@types/swagger-ui-react": "^5.18.0", "concurrently": "^9.2.0", - "eslint": "^9.31.0", + "eslint": "^9.32.0", "node-loader": "^2.1.0", "prettier": "^3.6.2", - "typescript": "^5.8.3" + "typescript": "^5.9.2" } } diff --git a/apps/nextjs/src/app/api/user-medias/[id]/route.ts b/apps/nextjs/src/app/api/user-medias/[id]/route.ts index f00b3e089..f4ad85ba8 100644 --- a/apps/nextjs/src/app/api/user-medias/[id]/route.ts +++ b/apps/nextjs/src/app/api/user-medias/[id]/route.ts @@ -23,7 +23,7 @@ export async function GET(_req: NextRequest, props: { params: Promise<{ id: stri headers.set("Content-Type", image.contentType); headers.set("Content-Length", image.content.length.toString()); - return new NextResponse(image.content, { + return new NextResponse(new Uint8Array(image.content), { status: 200, headers, }); diff --git a/apps/tasks/package.json b/apps/tasks/package.json index 063887e8c..5f8125a55 100644 --- a/apps/tasks/package.json +++ b/apps/tasks/package.json @@ -30,6 +30,7 @@ "@homarr/icons": "workspace:^0.1.0", "@homarr/integrations": "workspace:^0.1.0", "@homarr/log": "workspace:^", + "@homarr/ping": "workspace:^0.1.0", "@homarr/redis": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", @@ -38,18 +39,18 @@ "dotenv": "^17.2.1", "fastify": "^5.4.0", "superjson": "2.2.2", - "undici": "7.12.0" + "undici": "7.13.0" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "@types/node": "^22.16.4", - "dotenv-cli": "^8.0.0", + "@types/node": "^22.17.0", + "dotenv-cli": "^10.0.0", "esbuild": "^0.25.8", - "eslint": "^9.31.0", + "eslint": "^9.32.0", "prettier": "^3.6.2", "tsx": "4.20.3", - "typescript": "^5.8.3" + "typescript": "^5.9.2" } } diff --git a/apps/websocket/package.json b/apps/websocket/package.json index a28b53af5..5539c318c 100644 --- a/apps/websocket/package.json +++ b/apps/websocket/package.json @@ -35,8 +35,8 @@ "@homarr/tsconfig": "workspace:^0.1.0", "@types/ws": "^8.18.1", "esbuild": "^0.25.8", - "eslint": "^9.31.0", + "eslint": "^9.32.0", "prettier": "^3.6.2", - "typescript": "^5.8.3" + "typescript": "^5.9.2" } } diff --git a/package.json b/package.json index 7f8c6fcf4..fea254479 100644 --- a/package.json +++ b/package.json @@ -44,19 +44,19 @@ "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", "conventional-changelog-conventionalcommits": "^9.1.0", - "cross-env": "^7.0.3", + "cross-env": "^10.0.0", "jsdom": "^26.1.0", "prettier": "^3.6.2", "semantic-release": "^24.2.7", "testcontainers": "^11.4.0", "turbo": "^2.5.5", - "typescript": "^5.8.3", + "typescript": "^5.9.2", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.2.4" }, - "packageManager": "pnpm@10.13.1", + "packageManager": "pnpm@10.14.0", "engines": { - "node": ">=22.17.1" + "node": ">=22.18.0" }, "pnpm": { "onlyBuiltDependencies": [ @@ -71,7 +71,7 @@ "tree-sitter-json" ], "overrides": { - "proxmox-api>undici": "7.12.0" + "proxmox-api>undici": "7.13.0" }, "allowUnusedPatches": true, "ignoredBuiltDependencies": [ diff --git a/packages/analytics/package.json b/packages/analytics/package.json index 714a922e5..98a1437e9 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -32,7 +32,7 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/api/package.json b/packages/api/package.json index d92628cdf..49e1fe53e 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -35,20 +35,21 @@ "@homarr/log": "workspace:^", "@homarr/old-import": "workspace:^0.1.0", "@homarr/old-schema": "workspace:^0.1.0", + "@homarr/ping": "workspace:^0.1.0", "@homarr/redis": "workspace:^0.1.0", "@homarr/request-handler": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", "@kubernetes/client-node": "^1.3.0", - "@tanstack/react-query": "^5.83.0", + "@tanstack/react-query": "^5.84.1", "@trpc/client": "^11.4.3", "@trpc/react-query": "^11.4.3", "@trpc/server": "^11.4.3", "@trpc/tanstack-react-query": "^11.4.3", "lodash.clonedeep": "^4.5.0", - "next": "15.4.4", - "react": "19.1.0", - "react-dom": "19.1.0", + "next": "15.4.5", + "react": "19.1.1", + "react-dom": "19.1.1", "superjson": "2.2.2", "trpc-to-openapi": "^2.3.2", "zod": "^3.25.76" @@ -57,8 +58,8 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", + "eslint": "^9.32.0", "prettier": "^3.6.2", - "typescript": "^5.8.3" + "typescript": "^5.9.2" } } diff --git a/packages/api/src/router/test/widgets/app.spec.ts b/packages/api/src/router/test/widgets/app.spec.ts new file mode 100644 index 000000000..bd5265ff6 --- /dev/null +++ b/packages/api/src/router/test/widgets/app.spec.ts @@ -0,0 +1,53 @@ +import { describe, expect, test, vi } from "vitest"; + +import type { Session } from "@homarr/auth"; +import { createDb } from "@homarr/db/test"; +import * as ping from "@homarr/ping"; + +import { appRouter } from "../../widgets/app"; + +// Mock the auth module to return an empty session +vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session })); +vi.mock("@homarr/ping", () => ({ sendPingRequestAsync: async () => await Promise.resolve(null) })); + +describe("ping should call sendPingRequestAsync with url and return result", () => { + test("ping with error response should return error and url", async () => { + // Arrange + const spy = vi.spyOn(ping, "sendPingRequestAsync"); + const url = "http://localhost"; + const db = createDb(); + const caller = appRouter.createCaller({ + db, + deviceType: undefined, + session: null, + }); + spy.mockImplementation(() => Promise.resolve({ error: "error" })); + + // Act + const result = await caller.ping({ url }); + + // Assert + expect(result.url).toBe(url); + expect("error" in result).toBe(true); + }); + + test("ping with success response should return statusCode and url", async () => { + // Arrange + const spy = vi.spyOn(ping, "sendPingRequestAsync"); + const url = "http://localhost"; + const db = createDb(); + const caller = appRouter.createCaller({ + db, + deviceType: undefined, + session: null, + }); + spy.mockImplementation(() => Promise.resolve({ statusCode: 200, durationMs: 123 })); + + // Act + const result = await caller.ping({ url }); + + // Assert + expect(result.url).toBe(url); + expect("statusCode" in result).toBe(true); + }); +}); diff --git a/packages/api/src/router/widgets/app.ts b/packages/api/src/router/widgets/app.ts index a337b2e54..fa2ed05b4 100644 --- a/packages/api/src/router/widgets/app.ts +++ b/packages/api/src/router/widgets/app.ts @@ -1,12 +1,20 @@ import { observable } from "@trpc/server/observable"; import { z } from "zod"; -import { pingUrlChannel } from "@homarr/redis"; -import { pingRequestHandler } from "@homarr/request-handler/ping"; +import { sendPingRequestAsync } from "@homarr/ping"; +import { pingChannel, pingUrlChannel } from "@homarr/redis"; import { createTRPCRouter, publicProcedure } from "../../trpc"; export const appRouter = createTRPCRouter({ + ping: publicProcedure.input(z.object({ url: z.string() })).query(async ({ input }) => { + const pingResult = await sendPingRequestAsync(input.url); + + return { + url: input.url, + ...pingResult, + }; + }), updatedPing: publicProcedure .input( z.object({ @@ -15,20 +23,16 @@ export const appRouter = createTRPCRouter({ ) .subscription(async ({ input }) => { await pingUrlChannel.addAsync(input.url); - const innerHandler = pingRequestHandler.handler({ url: input.url }); + + const pingResult = await sendPingRequestAsync(input.url); return observable<{ url: string; statusCode: number; durationMs: number } | { url: string; error: string }>( (emit) => { - // Run ping request in background - void innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }).then(({ data }) => { - emit.next({ url: input.url, ...data }); - }); - - const unsubscribe = innerHandler.subscribe((pingResponse) => { - emit.next({ - url: input.url, - ...pingResponse, - }); + emit.next({ url: input.url, ...pingResult }); + const unsubscribe = pingChannel.subscribe((message) => { + // Only emit if same url + if (message.url !== input.url) return; + emit.next(message); }); return () => { diff --git a/packages/api/src/router/widgets/firewall.ts b/packages/api/src/router/widgets/firewall.ts new file mode 100644 index 000000000..2d582cb1f --- /dev/null +++ b/packages/api/src/router/widgets/firewall.ts @@ -0,0 +1,215 @@ +import { observable } from "@trpc/server/observable"; + +import type { Modify } from "@homarr/common/types"; +import type { Integration } from "@homarr/db/schema"; +import type { IntegrationKindByCategory } from "@homarr/definitions"; +import { getIntegrationKindsByCategory } from "@homarr/definitions"; +import type { + FirewallCpuSummary, + FirewallInterfacesSummary, + FirewallMemorySummary, + FirewallVersionSummary, +} from "@homarr/integrations"; +import { + firewallCpuRequestHandler, + firewallInterfacesRequestHandler, + firewallMemoryRequestHandler, + firewallVersionRequestHandler, +} from "@homarr/request-handler/firewall"; + +import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { createTRPCRouter, publicProcedure } from "../../trpc"; + +export const firewallRouter = createTRPCRouter({ + getFirewallCpuStatus: publicProcedure + .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) + .query(async ({ ctx }) => { + const results = await Promise.all( + ctx.integrations.map(async (integration) => { + const innerHandler = firewallCpuRequestHandler.handler(integration, {}); + const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + + return { + integration: { + id: integration.id, + name: integration.name, + kind: integration.kind, + updatedAt: timestamp, + }, + summary: data, + }; + }), + ); + return results; + }), + subscribeFirewallCpuStatus: publicProcedure + .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) + .subscription(({ ctx }) => { + return observable<{ + integration: Modify }>; + summary: FirewallCpuSummary; + }>((emit) => { + const unsubscribes: (() => void)[] = []; + for (const integrationWithSecrets of ctx.integrations) { + const { decryptedSecrets: _, ...integration } = integrationWithSecrets; + const innerHandler = firewallCpuRequestHandler.handler(integrationWithSecrets, {}); + const unsubscribe = innerHandler.subscribe((summary) => { + emit.next({ + integration, + summary, + }); + }); + unsubscribes.push(unsubscribe); + } + return () => { + unsubscribes.forEach((unsubscribe) => { + unsubscribe(); + }); + }; + }); + }), + + getFirewallInterfacesStatus: publicProcedure + .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) + .query(async ({ ctx }) => { + const results = await Promise.all( + ctx.integrations.map(async (integration) => { + const innerHandler = firewallInterfacesRequestHandler.handler(integration, {}); + const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + + return { + integration: { + id: integration.id, + name: integration.name, + kind: integration.kind, + updatedAt: timestamp, + }, + summary: data, + }; + }), + ); + return results; + }), + subscribeFirewallInterfacesStatus: publicProcedure + .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) + .subscription(({ ctx }) => { + return observable<{ + integration: Modify }>; + summary: FirewallInterfacesSummary[]; + }>((emit) => { + const unsubscribes: (() => void)[] = []; + for (const integrationWithSecrets of ctx.integrations) { + const { decryptedSecrets: _, ...integration } = integrationWithSecrets; + const innerHandler = firewallInterfacesRequestHandler.handler(integrationWithSecrets, {}); + const unsubscribe = innerHandler.subscribe((summary) => { + emit.next({ + integration, + summary, + }); + }); + unsubscribes.push(unsubscribe); + } + return () => { + unsubscribes.forEach((unsubscribe) => { + unsubscribe(); + }); + }; + }); + }), + + getFirewallVersionStatus: publicProcedure + .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) + .query(async ({ ctx }) => { + const results = await Promise.all( + ctx.integrations.map(async (integration) => { + const innerHandler = firewallVersionRequestHandler.handler(integration, {}); + const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + + return { + integration: { + id: integration.id, + name: integration.name, + kind: integration.kind, + updatedAt: timestamp, + }, + summary: data, + }; + }), + ); + return results; + }), + subscribeFirewallVersionStatus: publicProcedure + .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) + .subscription(({ ctx }) => { + return observable<{ + integration: Modify }>; + summary: FirewallVersionSummary; + }>((emit) => { + const unsubscribes: (() => void)[] = []; + for (const integrationWithSecrets of ctx.integrations) { + const { decryptedSecrets: _, ...integration } = integrationWithSecrets; + const innerHandler = firewallVersionRequestHandler.handler(integrationWithSecrets, {}); + const unsubscribe = innerHandler.subscribe((summary) => { + emit.next({ + integration, + summary, + }); + }); + unsubscribes.push(unsubscribe); + } + return () => { + unsubscribes.forEach((unsubscribe) => { + unsubscribe(); + }); + }; + }); + }), + + getFirewallMemoryStatus: publicProcedure + .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) + .query(async ({ ctx }) => { + const results = await Promise.all( + ctx.integrations.map(async (integration) => { + const innerHandler = firewallMemoryRequestHandler.handler(integration, {}); + const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + + return { + integration: { + id: integration.id, + name: integration.name, + kind: integration.kind, + updatedAt: timestamp, + }, + summary: data, + }; + }), + ); + return results; + }), + subscribeFirewallMemoryStatus: publicProcedure + .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) + .subscription(({ ctx }) => { + return observable<{ + integration: Modify }>; + summary: FirewallMemorySummary; + }>((emit) => { + const unsubscribes: (() => void)[] = []; + for (const integrationWithSecrets of ctx.integrations) { + const { decryptedSecrets: _, ...integration } = integrationWithSecrets; + const innerHandler = firewallMemoryRequestHandler.handler(integrationWithSecrets, {}); + const unsubscribe = innerHandler.subscribe((summary) => { + emit.next({ + integration, + summary, + }); + }); + unsubscribes.push(unsubscribe); + } + return () => { + unsubscribes.forEach((unsubscribe) => { + unsubscribe(); + }); + }; + }); + }), +}); diff --git a/packages/api/src/router/widgets/index.ts b/packages/api/src/router/widgets/index.ts index 2ad920b5d..8175aaab3 100644 --- a/packages/api/src/router/widgets/index.ts +++ b/packages/api/src/router/widgets/index.ts @@ -3,6 +3,7 @@ import { appRouter } from "./app"; import { calendarRouter } from "./calendar"; import { dnsHoleRouter } from "./dns-hole"; import { downloadsRouter } from "./downloads"; +import { firewallRouter } from "./firewall"; import { healthMonitoringRouter } from "./health-monitoring"; import { indexerManagerRouter } from "./indexer-manager"; import { mediaReleaseRouter } from "./media-release"; @@ -40,5 +41,6 @@ export const widgetRouter = createTRPCRouter({ options: optionsRouter, releases: releasesRouter, networkController: networkControllerRouter, + firewall: firewallRouter, notifications: notificationsRouter, }); diff --git a/packages/auth/package.json b/packages/auth/package.json index b66172618..1389eeab9 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -34,21 +34,21 @@ "@homarr/validation": "workspace:^0.1.0", "bcrypt": "^6.0.0", "cookies": "^0.9.1", - "ldapts": "8.0.8", - "next": "15.4.4", + "ldapts": "8.0.9", + "next": "15.4.5", "next-auth": "5.0.0-beta.29", - "react": "19.1.0", - "react-dom": "19.1.0", + "react": "19.1.1", + "react-dom": "19.1.1", "zod": "^3.25.76" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "@types/bcrypt": "5.0.2", + "@types/bcrypt": "6.0.0", "@types/cookies": "0.9.1", - "eslint": "^9.31.0", + "eslint": "^9.32.0", "prettier": "^3.6.2", - "typescript": "^5.8.3" + "typescript": "^5.9.2" } } diff --git a/packages/boards/package.json b/packages/boards/package.json index 7c9449505..9feba8d9b 100644 --- a/packages/boards/package.json +++ b/packages/boards/package.json @@ -25,14 +25,14 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/api": "workspace:^0.1.0", - "react": "19.1.0", - "react-dom": "19.1.0" + "react": "19.1.1", + "react-dom": "19.1.1" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/certificates/package.json b/packages/certificates/package.json index b9711a0e7..91b5b5a2b 100644 --- a/packages/certificates/package.json +++ b/packages/certificates/package.json @@ -24,13 +24,13 @@ "dependencies": { "@homarr/common": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0", - "undici": "7.12.0" + "undici": "7.13.0" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/certificates/src/server.ts b/packages/certificates/src/server.ts index 419a382c0..a138f4039 100644 --- a/packages/certificates/src/server.ts +++ b/packages/certificates/src/server.ts @@ -6,6 +6,7 @@ import { Agent as HttpsAgent } from "node:https"; import path from "node:path"; import { checkServerIdentity, rootCertificates } from "node:tls"; import axios from "axios"; +import type { RequestInfo, RequestInit, Response } from "undici"; import { fetch } from "undici"; import { env } from "@homarr/common/env"; @@ -131,8 +132,8 @@ export const createAxiosCertificateInstanceAsync = async ( }); }; -export const fetchWithTrustedCertificatesAsync: typeof fetch = async (url, options) => { - const agent = await createCertificateAgentAsync(); +export const fetchWithTrustedCertificatesAsync = async (url: RequestInfo, options?: RequestInit): Promise => { + const agent = await createCertificateAgentAsync(undefined); return fetch(url, { ...options, dispatcher: agent, diff --git a/packages/cli/package.json b/packages/cli/package.json index b5188b928..c5448a3fd 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -35,7 +35,7 @@ "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "esbuild": "^0.25.8", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/common/package.json b/packages/common/package.json index e00f9896a..d5effd335 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -31,10 +31,10 @@ "@homarr/log": "workspace:^0.1.0", "@paralleldrive/cuid2": "^2.2.2", "dayjs": "^1.11.13", - "next": "15.4.4", - "react": "19.1.0", - "react-dom": "19.1.0", - "undici": "7.12.0", + "next": "15.4.5", + "react": "19.1.1", + "react-dom": "19.1.1", + "undici": "7.13.0", "zod": "^3.25.76", "zod-validation-error": "^3.5.3" }, @@ -42,7 +42,7 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/core/package.json b/packages/core/package.json index 92183a1a1..2464293b3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -25,14 +25,14 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@t3-oss/env-nextjs": "^0.13.8", - "ioredis": "5.6.1", + "ioredis": "5.7.0", "zod": "^3.25.76" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/cron-job-api/package.json b/packages/cron-job-api/package.json index 88e248fbb..f7b3f79ed 100644 --- a/packages/cron-job-api/package.json +++ b/packages/cron-job-api/package.json @@ -29,12 +29,12 @@ "@homarr/core": "workspace:^0.1.0", "@homarr/cron-jobs": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0", - "@tanstack/react-query": "^5.83.0", + "@tanstack/react-query": "^5.84.1", "@trpc/client": "^11.4.3", "@trpc/server": "^11.4.3", "@trpc/tanstack-react-query": "^11.4.3", "node-cron": "^4.2.1", - "react": "19.1.0", + "react": "19.1.1", "zod": "^3.25.76" }, "devDependencies": { @@ -42,8 +42,8 @@ "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "@types/node-cron": "^3.0.11", - "@types/react": "19.1.8", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "@types/react": "19.1.9", + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/cron-job-status/package.json b/packages/cron-job-status/package.json index 35720a390..632f4e6b6 100644 --- a/packages/cron-job-status/package.json +++ b/packages/cron-job-status/package.json @@ -29,7 +29,7 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/cron-jobs-core/package.json b/packages/cron-jobs-core/package.json index 6fd35fd39..3acd05533 100644 --- a/packages/cron-jobs-core/package.json +++ b/packages/cron-jobs-core/package.json @@ -33,7 +33,7 @@ "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "@types/node-cron": "^3.0.11", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/cron-jobs-core/src/expressions.ts b/packages/cron-jobs-core/src/expressions.ts index e8015b486..59c500b67 100644 --- a/packages/cron-jobs-core/src/expressions.ts +++ b/packages/cron-jobs-core/src/expressions.ts @@ -1,6 +1,7 @@ import { checkCron } from "./validation"; export const EVERY_5_SECONDS = checkCron("*/5 * * * * *") satisfies string; +export const EVERY_30_SECONDS = checkCron("*/30 * * * * *") satisfies string; export const EVERY_MINUTE = checkCron("* * * * *") satisfies string; export const EVERY_5_MINUTES = checkCron("*/5 * * * *") satisfies string; export const EVERY_10_MINUTES = checkCron("*/10 * * * *") satisfies string; diff --git a/packages/cron-jobs/package.json b/packages/cron-jobs/package.json index 96f7667c7..c10da47b9 100644 --- a/packages/cron-jobs/package.json +++ b/packages/cron-jobs/package.json @@ -32,6 +32,7 @@ "@homarr/icons": "workspace:^0.1.0", "@homarr/integrations": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0", + "@homarr/ping": "workspace:^0.1.0", "@homarr/redis": "workspace:^0.1.0", "@homarr/request-handler": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0", @@ -43,7 +44,7 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/cron-jobs/src/index.ts b/packages/cron-jobs/src/index.ts index b29e8a46e..edc542db8 100644 --- a/packages/cron-jobs/src/index.ts +++ b/packages/cron-jobs/src/index.ts @@ -3,6 +3,12 @@ import { dockerContainersJob } from "./jobs/docker"; import { iconsUpdaterJob } from "./jobs/icons-updater"; import { dnsHoleJob } from "./jobs/integrations/dns-hole"; import { downloadsJob } from "./jobs/integrations/downloads"; +import { + firewallCpuJob, + firewallInterfacesJob, + firewallMemoryJob, + firewallVersionJob, +} from "./jobs/integrations/firewall"; import { healthMonitoringJob } from "./jobs/integrations/health-monitoring"; import { smartHomeEntityStateJob } from "./jobs/integrations/home-assistant"; import { indexerManagerJob } from "./jobs/integrations/indexer-manager"; @@ -39,6 +45,10 @@ export const jobGroup = createCronJobGroup({ minecraftServerStatus: minecraftServerStatusJob, dockerContainers: dockerContainersJob, networkController: networkControllerJob, + firewallCpu: firewallCpuJob, + firewallMemory: firewallMemoryJob, + firewallVersion: firewallVersionJob, + firewallInterfaces: firewallInterfacesJob, refreshNotifications: refreshNotificationsJob, }); diff --git a/packages/cron-jobs/src/jobs/integrations/firewall.ts b/packages/cron-jobs/src/jobs/integrations/firewall.ts new file mode 100644 index 000000000..50637a642 --- /dev/null +++ b/packages/cron-jobs/src/jobs/integrations/firewall.ts @@ -0,0 +1,46 @@ +import { EVERY_5_SECONDS, EVERY_30_SECONDS, EVERY_HOUR, EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; +import { + firewallCpuRequestHandler, + firewallInterfacesRequestHandler, + firewallMemoryRequestHandler, + firewallVersionRequestHandler, +} from "@homarr/request-handler/firewall"; +import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; + +import { createCronJob } from "../../lib"; + +export const firewallCpuJob = createCronJob("firewallCpu", EVERY_5_SECONDS).withCallback( + createRequestIntegrationJobHandler(firewallCpuRequestHandler.handler, { + widgetKinds: ["firewall"], + getInput: { + firewall: () => ({}), + }, + }), +); + +export const firewallMemoryJob = createCronJob("firewallMemory", EVERY_MINUTE).withCallback( + createRequestIntegrationJobHandler(firewallMemoryRequestHandler.handler, { + widgetKinds: ["firewall"], + getInput: { + firewall: () => ({}), + }, + }), +); + +export const firewallInterfacesJob = createCronJob("firewallInterfaces", EVERY_30_SECONDS).withCallback( + createRequestIntegrationJobHandler(firewallInterfacesRequestHandler.handler, { + widgetKinds: ["firewall"], + getInput: { + firewall: () => ({}), + }, + }), +); + +export const firewallVersionJob = createCronJob("firewallVersion", EVERY_HOUR).withCallback( + createRequestIntegrationJobHandler(firewallVersionRequestHandler.handler, { + widgetKinds: ["firewall"], + getInput: { + firewall: () => ({}), + }, + }), +); diff --git a/packages/cron-jobs/src/jobs/ping.ts b/packages/cron-jobs/src/jobs/ping.ts index 52a85b2c5..318db80bd 100644 --- a/packages/cron-jobs/src/jobs/ping.ts +++ b/packages/cron-jobs/src/jobs/ping.ts @@ -2,8 +2,8 @@ import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; import { db } from "@homarr/db"; import { getServerSettingByKeyAsync } from "@homarr/db/queries"; import { logger } from "@homarr/log"; -import { pingUrlChannel } from "@homarr/redis"; -import { pingRequestHandler } from "@homarr/request-handler/ping"; +import { sendPingRequestAsync } from "@homarr/ping"; +import { pingChannel, pingUrlChannel } from "@homarr/redis"; import { createCronJob } from "../lib"; @@ -28,6 +28,16 @@ export const pingJob = createCronJob("ping", EVERY_MINUTE, { }); const pingAsync = async (url: string) => { - const handler = pingRequestHandler.handler({ url }); - await handler.getCachedOrUpdatedDataAsync({ forceUpdate: true }); + const pingResult = await sendPingRequestAsync(url); + + if ("statusCode" in pingResult) { + logger.debug(`executed ping for url ${url} with status code ${pingResult.statusCode}`); + } else { + logger.error(`Executing ping for url ${url} failed with error: ${pingResult.error}`); + } + + await pingChannel.publishAsync({ + url, + ...pingResult, + }); }; diff --git a/packages/db/package.json b/packages/db/package.json index 75a1f142b..724832570 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -44,15 +44,15 @@ "@homarr/definitions": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0", - "@mantine/core": "^8.2.1", + "@mantine/core": "^8.2.2", "@paralleldrive/cuid2": "^2.2.2", "@testcontainers/mysql": "^11.4.0", "better-sqlite3": "^12.2.0", "dotenv": "^17.2.1", "drizzle-kit": "^0.31.4", - "drizzle-orm": "^0.44.3", + "drizzle-orm": "^0.44.4", "drizzle-zod": "^0.7.1", - "mysql2": "3.14.2", + "mysql2": "3.14.3", "superjson": "2.2.2" }, "devDependencies": { @@ -60,11 +60,11 @@ "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "@types/better-sqlite3": "7.6.13", - "dotenv-cli": "^8.0.0", + "dotenv-cli": "^10.0.0", "esbuild": "^0.25.8", - "eslint": "^9.31.0", + "eslint": "^9.32.0", "prettier": "^3.6.2", "tsx": "4.20.3", - "typescript": "^5.8.3" + "typescript": "^5.9.2" } } diff --git a/packages/definitions/package.json b/packages/definitions/package.json index 61df9b6c5..6a0e004fa 100644 --- a/packages/definitions/package.json +++ b/packages/definitions/package.json @@ -31,8 +31,8 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", + "eslint": "^9.32.0", "tsx": "4.20.3", - "typescript": "^5.8.3" + "typescript": "^5.9.2" } } diff --git a/packages/definitions/src/docs/homarr-docs-sitemap.ts b/packages/definitions/src/docs/homarr-docs-sitemap.ts index 1dc481a49..378a96c83 100644 --- a/packages/definitions/src/docs/homarr-docs-sitemap.ts +++ b/packages/definitions/src/docs/homarr-docs-sitemap.ts @@ -39,22 +39,16 @@ export type HomarrDocumentationPath = | "/search" | "/docs/tags" | "/docs/tags/active-directory" - | "/docs/tags/ad-guard" - | "/docs/tags/ad-guard-home" | "/docs/tags/administration" | "/docs/tags/advanced" | "/docs/tags/analytics" | "/docs/tags/api" | "/docs/tags/apps" | "/docs/tags/background" - | "/docs/tags/banner" - | "/docs/tags/blocking" | "/docs/tags/boards" - | "/docs/tags/bookmark" | "/docs/tags/bookmarks" | "/docs/tags/caddy" | "/docs/tags/certificates" - | "/docs/tags/checklist" | "/docs/tags/code" | "/docs/tags/community" | "/docs/tags/configuration" @@ -64,63 +58,37 @@ export type HomarrDocumentationPath = | "/docs/tags/database" | "/docs/tags/developer" | "/docs/tags/development" - | "/docs/tags/dns" | "/docs/tags/docker" | "/docs/tags/donation" | "/docs/tags/edit-mode" | "/docs/tags/env" | "/docs/tags/environment-variables" - | "/docs/tags/feeds" - | "/docs/tags/finance" | "/docs/tags/getting-started" | "/docs/tags/google" - | "/docs/tags/grafana" | "/docs/tags/groups" - | "/docs/tags/hardware" - | "/docs/tags/health" | "/docs/tags/help" | "/docs/tags/icon-picker" | "/docs/tags/icon-repositories" | "/docs/tags/icons" - | "/docs/tags/iframe" - | "/docs/tags/images" | "/docs/tags/installation" - | "/docs/tags/integrade" | "/docs/tags/integration" | "/docs/tags/integrations" | "/docs/tags/interface" - | "/docs/tags/jellyserr" | "/docs/tags/jobs" | "/docs/tags/layout" | "/docs/tags/ldap" - | "/docs/tags/links" - | "/docs/tags/lists" | "/docs/tags/management" - | "/docs/tags/market" | "/docs/tags/media" - | "/docs/tags/minecraft" - | "/docs/tags/monitoring" - | "/docs/tags/network" - | "/docs/tags/news" - | "/docs/tags/notebook" - | "/docs/tags/notes" | "/docs/tags/oidc" | "/docs/tags/open-collective" - | "/docs/tags/open-media-vault" - | "/docs/tags/overseerr" | "/docs/tags/permissions" | "/docs/tags/pgid" - | "/docs/tags/pi-hole" | "/docs/tags/ping" | "/docs/tags/programming" - | "/docs/tags/proxmox" | "/docs/tags/proxy" | "/docs/tags/puid" - | "/docs/tags/releases" - | "/docs/tags/repositories" | "/docs/tags/responsive" | "/docs/tags/roles" - | "/docs/tags/rss" | "/docs/tags/search" | "/docs/tags/search-engines" | "/docs/tags/security" @@ -128,24 +96,15 @@ export type HomarrDocumentationPath = | "/docs/tags/seo" | "/docs/tags/server" | "/docs/tags/settings" - | "/docs/tags/sinkhole" | "/docs/tags/sso" - | "/docs/tags/stocks" - | "/docs/tags/system" - | "/docs/tags/table" | "/docs/tags/tasks" | "/docs/tags/technical-documentation" - | "/docs/tags/text" - | "/docs/tags/torrent" | "/docs/tags/traefik" | "/docs/tags/translations" - | "/docs/tags/unifi-controller" | "/docs/tags/unraid" | "/docs/tags/uploads" - | "/docs/tags/usenet" | "/docs/tags/users" | "/docs/tags/variables" - | "/docs/tags/widgets" | "/docs/advanced/command-line" | "/docs/advanced/command-line/fix-usernames" | "/docs/advanced/command-line/password-recovery" @@ -188,17 +147,38 @@ export type HomarrDocumentationPath = | "/docs/getting-started/installation/source" | "/docs/getting-started/installation/synology" | "/docs/getting-started/installation/unraid" - | "/docs/integrations/cloud" - | "/docs/integrations/containers" - | "/docs/integrations/dns" - | "/docs/integrations/hardware" + | "/docs/integrations/adguard-home" + | "/docs/integrations/codeberg" + | "/docs/integrations/dash-dot" + | "/docs/integrations/deluge" + | "/docs/integrations/docker-hub" + | "/docs/integrations/docker" + | "/docs/integrations/emby" + | "/docs/integrations/github" + | "/docs/integrations/gitlab" + | "/docs/integrations/home-assistant" + | "/docs/integrations/jellyfin" + | "/docs/integrations/jellyseerr" | "/docs/integrations/kubernetes" - | "/docs/integrations/media-requester" - | "/docs/integrations/media-server" - | "/docs/integrations/network" - | "/docs/integrations/servarr" - | "/docs/integrations/torrent" - | "/docs/integrations/usenet" + | "/docs/integrations/lidarr" + | "/docs/integrations/nextcloud" + | "/docs/integrations/npm" + | "/docs/integrations/ntfy" + | "/docs/integrations/nzbget" + | "/docs/integrations/open-media-vault" + | "/docs/integrations/overseerr" + | "/docs/integrations/pi-hole" + | "/docs/integrations/plex" + | "/docs/integrations/prowlarr" + | "/docs/integrations/proxmox" + | "/docs/integrations/q-bittorent" + | "/docs/integrations/radarr" + | "/docs/integrations/readarr" + | "/docs/integrations/sabnzbd" + | "/docs/integrations/sonarr" + | "/docs/integrations/tdarr" + | "/docs/integrations/transmission" + | "/docs/integrations/unifi-controller" | "/docs/management/api" | "/docs/management/apps" | "/docs/management/boards" @@ -209,23 +189,32 @@ export type HomarrDocumentationPath = | "/docs/management/settings" | "/docs/management/tasks" | "/docs/management/users" + | "/docs/widgets/app" | "/docs/widgets/bookmarks" | "/docs/widgets/calendar" | "/docs/widgets/clock" - | "/docs/widgets/dns-hole" + | "/docs/widgets/dns-hole-controls" + | "/docs/widgets/dns-hole-summary" + | "/docs/widgets/docker-containers" | "/docs/widgets/downloads" | "/docs/widgets/health-monitoring" - | "/docs/widgets/home-assistant" | "/docs/widgets/iframe" | "/docs/widgets/indexer-manager" - | "/docs/widgets/media-requests" + | "/docs/widgets/media-releases" + | "/docs/widgets/media-request-list" + | "/docs/widgets/media-request-stats" | "/docs/widgets/media-server" + | "/docs/widgets/media-transcoding" | "/docs/widgets/minecraft-server-status" - | "/docs/widgets/network-controller" + | "/docs/widgets/network-controller-status" + | "/docs/widgets/network-controller-summary" | "/docs/widgets/notebook" + | "/docs/widgets/notifications" | "/docs/widgets/releases" - | "/docs/widgets/rss" - | "/docs/widgets/stocks" + | "/docs/widgets/rss-feed" + | "/docs/widgets/smart-home-entity-state" + | "/docs/widgets/smart-home-execute-automation" + | "/docs/widgets/stock-price" | "/docs/widgets/video" | "/docs/widgets/weather" | "" diff --git a/packages/definitions/src/integration.ts b/packages/definitions/src/integration.ts index 7a1112aba..4bf7adada 100644 --- a/packages/definitions/src/integration.ts +++ b/packages/definitions/src/integration.ts @@ -172,6 +172,12 @@ export const integrationDefs = { iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/unifi.png", category: ["networkController"], }, + opnsense: { + name: "OPNsense", + secretKinds: [["username", "password"]], + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/opnsense.svg", + category: ["firewall"], + }, github: { name: "Github", secretKinds: [[], ["personalAccessToken"]], @@ -207,6 +213,27 @@ export const integrationDefs = { category: ["releasesProvider"], defaultUrl: "https://codeberg.org", }, + linuxServerIO: { + name: "LinuxServer.io", + secretKinds: [[]], + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/linuxserver-io.svg", + category: ["releasesProvider"], + defaultUrl: "https://api.linuxserver.io", + }, + gitHubContainerRegistry: { + name: "GitHub Container Registry", + secretKinds: [[], ["personalAccessToken"]], + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/github.svg", + category: ["releasesProvider"], + defaultUrl: "https://api.github.com", + }, + quay: { + name: "Quay", + secretKinds: [[], ["personalAccessToken"]], + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/quay.png", + category: ["releasesProvider"], + defaultUrl: "https://quay.io", + }, ntfy: { name: "ntfy", secretKinds: [["topic"], ["topic", "apiKey"]], @@ -297,6 +324,7 @@ export const integrationCategories = [ "networkController", "releasesProvider", "notifications", + "firewall", ] as const; export type IntegrationCategory = (typeof integrationCategories)[number]; diff --git a/packages/definitions/src/widget.ts b/packages/definitions/src/widget.ts index 24da8f214..8bafbab57 100644 --- a/packages/definitions/src/widget.ts +++ b/packages/definitions/src/widget.ts @@ -26,6 +26,7 @@ export const widgetKinds = [ "releases", "mediaReleases", "dockerContainers", + "firewall", "notifications", ] as const; export type WidgetKind = (typeof widgetKinds)[number]; diff --git a/packages/docker/package.json b/packages/docker/package.json index 534211153..51f07d96d 100644 --- a/packages/docker/package.json +++ b/packages/docker/package.json @@ -33,7 +33,7 @@ "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "@types/dockerode": "^3.3.42", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/form/package.json b/packages/form/package.json index 39401839a..10ace02a1 100644 --- a/packages/form/package.json +++ b/packages/form/package.json @@ -26,14 +26,14 @@ "@homarr/common": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", - "@mantine/form": "^8.2.1", + "@mantine/form": "^8.2.2", "zod": "^3.25.76" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/forms-collection/package.json b/packages/forms-collection/package.json index 887073f28..42798f2ef 100644 --- a/packages/forms-collection/package.json +++ b/packages/forms-collection/package.json @@ -29,15 +29,15 @@ "@homarr/notifications": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", - "@mantine/core": "^8.2.1", - "react": "19.1.0", + "@mantine/core": "^8.2.2", + "react": "19.1.1", "zod": "^3.25.76" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/icons/package.json b/packages/icons/package.json index 30fdb487d..9c1823133 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -31,7 +31,7 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/image-proxy/package.json b/packages/image-proxy/package.json index 638d21704..7657f8797 100644 --- a/packages/image-proxy/package.json +++ b/packages/image-proxy/package.json @@ -32,8 +32,8 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "@types/bcrypt": "5.0.2", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "@types/bcrypt": "6.0.0", + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/integrations/package.json b/packages/integrations/package.json index 142c7dc0f..6d639944f 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -45,7 +45,7 @@ "octokit": "^5.0.3", "proxmox-api": "1.1.1", "tsdav": "^2.1.5", - "undici": "7.12.0", + "undici": "7.13.0", "xml2js": "^0.6.2", "zod": "^3.25.76" }, @@ -55,7 +55,7 @@ "@homarr/tsconfig": "workspace:^0.1.0", "@types/node-unifi": "^2.5.1", "@types/xml2js": "^0.4.14", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/integrations/src/base/creator.ts b/packages/integrations/src/base/creator.ts index dda1b03a5..a5a0f5d82 100644 --- a/packages/integrations/src/base/creator.ts +++ b/packages/integrations/src/base/creator.ts @@ -14,11 +14,13 @@ import { QBitTorrentIntegration } from "../download-client/qbittorrent/qbittorre import { SabnzbdIntegration } from "../download-client/sabnzbd/sabnzbd-integration"; import { TransmissionIntegration } from "../download-client/transmission/transmission-integration"; import { EmbyIntegration } from "../emby/emby-integration"; +import { GitHubContainerRegistryIntegration } from "../github-container-registry/github-container-registry-integration"; import { GithubIntegration } from "../github/github-integration"; import { GitlabIntegration } from "../gitlab/gitlab-integration"; import { HomeAssistantIntegration } from "../homeassistant/homeassistant-integration"; import { JellyfinIntegration } from "../jellyfin/jellyfin-integration"; import { JellyseerrIntegration } from "../jellyseerr/jellyseerr-integration"; +import { LinuxServerIOIntegration } from "../linuxserverio/linuxserverio-integration"; import { LidarrIntegration } from "../media-organizer/lidarr/lidarr-integration"; import { RadarrIntegration } from "../media-organizer/radarr/radarr-integration"; import { ReadarrIntegration } from "../media-organizer/readarr/readarr-integration"; @@ -29,11 +31,13 @@ import { NextcloudIntegration } from "../nextcloud/nextcloud.integration"; import { NPMIntegration } from "../npm/npm-integration"; import { NTFYIntegration } from "../ntfy/ntfy-integration"; import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration"; +import { OPNsenseIntegration } from "../opnsense/opnsense-integration"; import { OverseerrIntegration } from "../overseerr/overseerr-integration"; import { createPiHoleIntegrationAsync } from "../pi-hole/pi-hole-integration-factory"; import { PlexIntegration } from "../plex/plex-integration"; import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration"; import { ProxmoxIntegration } from "../proxmox/proxmox-integration"; +import { QuayIntegration } from "../quay/quay-integration"; import { UnifiControllerIntegration } from "../unifi-controller/unifi-controller-integration"; import type { Integration, IntegrationInput } from "./integration"; @@ -99,11 +103,15 @@ export const integrationCreators = { emby: EmbyIntegration, nextcloud: NextcloudIntegration, unifiController: UnifiControllerIntegration, + opnsense: OPNsenseIntegration, github: GithubIntegration, dockerHub: DockerHubIntegration, gitlab: GitlabIntegration, npm: NPMIntegration, codeberg: CodebergIntegration, + linuxServerIO: LinuxServerIOIntegration, + gitHubContainerRegistry: GitHubContainerRegistryIntegration, + quay: QuayIntegration, ntfy: NTFYIntegration, mock: MockIntegration, } satisfies Record Promise]>; diff --git a/packages/integrations/src/codeberg/codeberg-integration.ts b/packages/integrations/src/codeberg/codeberg-integration.ts index 08adfabc7..2836917a0 100644 --- a/packages/integrations/src/codeberg/codeberg-integration.ts +++ b/packages/integrations/src/codeberg/codeberg-integration.ts @@ -20,7 +20,7 @@ const localLogger = logger.child({ module: "CodebergIntegration" }); export class CodebergIntegration extends Integration implements ReleasesProviderIntegration { private async withHeadersAsync(callback: (headers: RequestInit["headers"]) => Promise): Promise { - if (!this.hasSecretValue("personalAccessToken")) return await callback({}); + if (!this.hasSecretValue("personalAccessToken")) return await callback(undefined); return await callback({ Authorization: `token ${this.getSecretValue("personalAccessToken")}`, @@ -61,7 +61,7 @@ export class CodebergIntegration extends Integration implements ReleasesProvider const details = await this.getDetailsAsync(owner, name); const releasesResponse = await this.withHeadersAsync(async (headers) => { - return fetchWithTrustedCertificatesAsync( + return await fetchWithTrustedCertificatesAsync( this.url(`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/releases`), { headers }, ); diff --git a/packages/integrations/src/docker-hub/docker-hub-integration.ts b/packages/integrations/src/docker-hub/docker-hub-integration.ts index 2affdf89d..a961ddc38 100644 --- a/packages/integrations/src/docker-hub/docker-hub-integration.ts +++ b/packages/integrations/src/docker-hub/docker-hub-integration.ts @@ -30,7 +30,8 @@ export class DockerHubIntegration extends Integration implements ReleasesProvide } private async withHeadersAsync(callback: (headers: RequestInit["headers"]) => Promise): Promise { - if (!this.hasSecretValue("username") || !this.hasSecretValue("personalAccessToken")) return await callback({}); + if (!this.hasSecretValue("username") || !this.hasSecretValue("personalAccessToken")) + return await callback(undefined); const storedSession = await this.sessionStore.getAsync(); diff --git a/packages/integrations/src/github-container-registry/github-container-registry-integration.ts b/packages/integrations/src/github-container-registry/github-container-registry-integration.ts new file mode 100644 index 000000000..826de6a2f --- /dev/null +++ b/packages/integrations/src/github-container-registry/github-container-registry-integration.ts @@ -0,0 +1,145 @@ +import { Octokit, RequestError } from "octokit"; + +import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { logger } from "@homarr/log"; + +import type { IntegrationTestingInput } from "../base/integration"; +import { Integration } from "../base/integration"; +import { TestConnectionError } from "../base/test-connection/test-connection-error"; +import type { TestingResult } from "../base/test-connection/test-connection-service"; +import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration"; +import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration"; +import type { + DetailsProviderResponse, + ReleaseProviderResponse, + ReleasesRepository, + ReleasesResponse, +} from "../interfaces/releases-providers/releases-providers-types"; + +const localLogger = logger.child({ module: "GitHubContainerRegistryIntegration" }); + +export class GitHubContainerRegistryIntegration extends Integration implements ReleasesProviderIntegration { + private static readonly userAgent = "Homarr-Lab/Homarr:GitHubContainerRegistryIntegration"; + + protected async testingAsync(input: IntegrationTestingInput): Promise { + const headers: RequestInit["headers"] = { + "User-Agent": GitHubContainerRegistryIntegration.userAgent, + }; + + if (this.hasSecretValue("personalAccessToken")) + headers.Authorization = `Bearer ${this.getSecretValue("personalAccessToken")}`; + + const response = await input.fetchAsync(this.url("/octocat"), { + headers, + }); + + if (!response.ok) { + return TestConnectionError.StatusResult(response); + } + + return { + success: true, + }; + } + + public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise { + const [owner, name] = repository.identifier.split("/"); + if (!owner || !name) { + localLogger.warn( + `Invalid identifier format. Expected 'owner/name', for ${repository.identifier} with GitHub Container Registry integration`, + { + identifier: repository.identifier, + }, + ); + return { + id: repository.id, + error: { code: "invalidIdentifier" }, + }; + } + + const api = this.getApi(); + const details = await this.getDetailsAsync(api, owner, name); + + try { + const releasesResponse = await api.rest.packages.getAllPackageVersionsForPackageOwnedByUser({ + username: owner, + package_type: "container", + package_name: name, + per_page: 100, + }); + + const releasesProviderResponse = releasesResponse.data.reduce((acc, release) => { + if (!release.metadata?.container?.tags || !(release.metadata.container.tags.length > 0)) return acc; + + release.metadata.container.tags.forEach((tag) => { + acc.push({ + latestRelease: tag, + latestReleaseAt: new Date(release.updated_at), + releaseUrl: release.html_url, + releaseDescription: release.description ?? undefined, + }); + }); + return acc; + }, []); + + return getLatestRelease(releasesProviderResponse, repository, details); + } catch (error) { + const errorMessage = error instanceof RequestError ? error.message : String(error); + + localLogger.warn(`Failed to get releases for ${owner}\\${name} with GitHub Container Registry integration`, { + owner, + name, + error: errorMessage, + }); + + return { + id: repository.id, + error: { message: errorMessage }, + }; + } + } + + protected async getDetailsAsync( + api: Octokit, + owner: string, + name: string, + ): Promise { + try { + const response = await api.rest.packages.getPackageForUser({ + username: owner, + package_type: "container", + package_name: name, + }); + + return { + projectUrl: response.data.repository?.html_url ?? response.data.html_url, + projectDescription: response.data.repository?.description ?? undefined, + isFork: response.data.repository?.fork, + isArchived: response.data.repository?.archived, + createdAt: new Date(response.data.created_at), + starsCount: response.data.repository?.stargazers_count, + openIssues: response.data.repository?.open_issues_count, + forksCount: response.data.repository?.forks_count, + }; + } catch (error) { + localLogger.warn(`Failed to get details for ${owner}\\${name} with GitHub Container Registry integration`, { + owner, + name, + error: error instanceof RequestError ? error.message : String(error), + }); + return undefined; + } + } + + private getApi() { + return new Octokit({ + baseUrl: this.url("/").origin, + request: { + fetch: fetchWithTrustedCertificatesAsync, + }, + userAgent: GitHubContainerRegistryIntegration.userAgent, + throttle: { enabled: false }, // Disable throttling for this integration, Octokit will retry by default after a set time, thus delaying the repsonse to the user in case of errors. Errors will be shown to the user, no need to retry the request. + ...(this.hasSecretValue("personalAccessToken") ? { auth: this.getSecretValue("personalAccessToken") } : {}), + }); + } +} diff --git a/packages/integrations/src/github/github-integration.ts b/packages/integrations/src/github/github-integration.ts index 97689518e..b97157904 100644 --- a/packages/integrations/src/github/github-integration.ts +++ b/packages/integrations/src/github/github-integration.ts @@ -58,7 +58,6 @@ export class GithubIntegration extends Integration implements ReleasesProviderIn } const api = this.getApi(); - const details = await this.getDetailsAsync(api, owner, name); try { diff --git a/packages/integrations/src/index.ts b/packages/integrations/src/index.ts index d11232186..713777607 100644 --- a/packages/integrations/src/index.ts +++ b/packages/integrations/src/index.ts @@ -21,13 +21,20 @@ export { PiHoleIntegrationV5 } from "./pi-hole/v5/pi-hole-integration-v5"; export { PiHoleIntegrationV6 } from "./pi-hole/v6/pi-hole-integration-v6"; export { PlexIntegration } from "./plex/plex-integration"; export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration"; +export { OPNsenseIntegration } from "./opnsense/opnsense-integration"; // Types export type { IntegrationInput } from "./base/integration"; export type { DownloadClientJobsAndStatus } from "./interfaces/downloads/download-client-data"; export type { ExtendedDownloadClientItem } from "./interfaces/downloads/download-client-items"; export type { ExtendedClientStatus } from "./interfaces/downloads/download-client-status"; - +export type { + FirewallInterface, + FirewallCpuSummary, + FirewallInterfacesSummary, + FirewallVersionSummary, + FirewallMemorySummary, +} from "./interfaces/firewall-summary/firewall-summary-types"; export type { SystemHealthMonitoring } from "./interfaces/health-monitoring/health-monitoring-types"; export { MediaRequestStatus } from "./interfaces/media-requests/media-request-types"; export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request-types"; diff --git a/packages/integrations/src/interfaces/firewall-summary/firewall-summary-integration.ts b/packages/integrations/src/interfaces/firewall-summary/firewall-summary-integration.ts new file mode 100644 index 000000000..463299ef5 --- /dev/null +++ b/packages/integrations/src/interfaces/firewall-summary/firewall-summary-integration.ts @@ -0,0 +1,13 @@ +import type { + FirewallCpuSummary, + FirewallInterfacesSummary, + FirewallMemorySummary, + FirewallVersionSummary, +} from "./firewall-summary-types"; + +export interface FirewallSummaryIntegration { + getFirewallCpuAsync(): Promise; + getFirewallMemoryAsync(): Promise; + getFirewallInterfacesAsync(): Promise; + getFirewallVersionAsync(): Promise; +} diff --git a/packages/integrations/src/interfaces/firewall-summary/firewall-summary-types.ts b/packages/integrations/src/interfaces/firewall-summary/firewall-summary-types.ts new file mode 100644 index 000000000..3106b2943 --- /dev/null +++ b/packages/integrations/src/interfaces/firewall-summary/firewall-summary-types.ts @@ -0,0 +1,24 @@ +export interface FirewallInterfacesSummary { + data: FirewallInterface[]; + timestamp: Date; +} + +export interface FirewallInterface { + name: string; + receive: number; + transmit: number; +} + +export interface FirewallVersionSummary { + version: string; +} + +export interface FirewallCpuSummary { + total: number; +} + +export interface FirewallMemorySummary { + used: number; + total: number; + percent: number; +} diff --git a/packages/integrations/src/linuxserverio/linuxserverio-integration.ts b/packages/integrations/src/linuxserverio/linuxserverio-integration.ts new file mode 100644 index 000000000..b3039881d --- /dev/null +++ b/packages/integrations/src/linuxserverio/linuxserverio-integration.ts @@ -0,0 +1,88 @@ +import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { logger } from "@homarr/log"; + +import type { IntegrationTestingInput } from "../base/integration"; +import { Integration } from "../base/integration"; +import { TestConnectionError } from "../base/test-connection/test-connection-error"; +import type { TestingResult } from "../base/test-connection/test-connection-service"; +import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration"; +import type { ReleasesRepository, ReleasesResponse } from "../interfaces/releases-providers/releases-providers-types"; +import { releasesResponseSchema } from "./linuxserverio-schemas"; + +const localLogger = logger.child({ module: "LinuxServerIOsIntegration" }); + +export class LinuxServerIOIntegration extends Integration implements ReleasesProviderIntegration { + protected async testingAsync(input: IntegrationTestingInput): Promise { + const response = await input.fetchAsync(this.url("/health")); + + if (!response.ok) { + return TestConnectionError.StatusResult(response); + } + + return { + success: true, + }; + } + + public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise { + const [owner, name] = repository.identifier.split("/"); + if (!owner || !name) { + localLogger.warn( + `Invalid identifier format. Expected 'owner/name', for ${repository.identifier} with LinuxServerIO integration`, + { + identifier: repository.identifier, + }, + ); + return { + id: repository.id, + error: { code: "invalidIdentifier" }, + }; + } + + const releasesResponse = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/images")); + + if (!releasesResponse.ok) { + return { + id: repository.id, + error: { message: releasesResponse.statusText }, + }; + } + + const releasesResponseJson: unknown = await releasesResponse.json(); + const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson); + + if (!success) { + return { + id: repository.id, + error: { + message: error.message, + }, + }; + } else { + const release = data.data.repositories.linuxserver.find((repo) => repo.name === name); + if (!release) { + localLogger.warn(`Repository ${name} not found on provider, with LinuxServerIO integration`, { + owner, + name, + }); + + return { + id: repository.id, + error: { code: "noReleasesFound" }, + }; + } + + return { + id: repository.id, + latestRelease: release.version, + latestReleaseAt: release.version_timestamp, + releaseDescription: release.changelog?.shift()?.desc, + projectUrl: release.github_url, + projectDescription: release.description, + isArchived: release.deprecated, + createdAt: release.initial_date ? new Date(release.initial_date) : undefined, + starsCount: release.stars, + }; + } + } +} diff --git a/packages/integrations/src/linuxserverio/linuxserverio-schemas.ts b/packages/integrations/src/linuxserverio/linuxserverio-schemas.ts new file mode 100644 index 000000000..bf9842a50 --- /dev/null +++ b/packages/integrations/src/linuxserverio/linuxserverio-schemas.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; + +export const releasesResponseSchema = z.object({ + data: z.object({ + repositories: z.object({ + linuxserver: z.array( + z.object({ + name: z.string(), + initial_date: z + .string() + .transform((value) => new Date(value)) + .optional(), + github_url: z.string(), + description: z.string(), + version: z.string(), + version_timestamp: z.string().transform((value) => new Date(value)), + stars: z.number(), + deprecated: z.boolean(), + changelog: z + .array( + z.object({ + date: z.string().transform((value) => new Date(value)), + desc: z.string(), + }), + ) + .optional(), + }), + ), + }), + }), +}); diff --git a/packages/integrations/src/opnsense/opnsense-integration.ts b/packages/integrations/src/opnsense/opnsense-integration.ts new file mode 100644 index 000000000..d1085141e --- /dev/null +++ b/packages/integrations/src/opnsense/opnsense-integration.ts @@ -0,0 +1,189 @@ +import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { ParseError, ResponseError } from "@homarr/common/server"; +import { createChannelEventHistory } from "@homarr/redis"; + +import { HandleIntegrationErrors } from "../base/errors/decorator"; +import type { IntegrationTestingInput } from "../base/integration"; +import { Integration } from "../base/integration"; +import { TestConnectionError } from "../base/test-connection/test-connection-error"; +import type { TestingResult } from "../base/test-connection/test-connection-service"; +import type { FirewallSummaryIntegration } from "../interfaces/firewall-summary/firewall-summary-integration"; +import type { + FirewallCpuSummary, + FirewallInterface, + FirewallInterfacesSummary, + FirewallMemorySummary, + FirewallVersionSummary, +} from "../interfaces/firewall-summary/firewall-summary-types"; +import { + opnsenseCPUSchema, + opnsenseInterfacesSchema, + opnsenseMemorySchema, + opnsenseSystemSummarySchema, +} from "./opnsense-types"; + +@HandleIntegrationErrors([]) +export class OPNsenseIntegration extends Integration implements FirewallSummaryIntegration { + protected async testingAsync(input: IntegrationTestingInput): Promise { + const response = await input.fetchAsync(this.url("/api/diagnostics/system/system_information"), { + headers: { + Authorization: this.getAuthHeaders(), + }, + }); + if (!response.ok) return TestConnectionError.StatusResult(response); + + const result = await response.json(); + if (typeof result === "object" && result !== null) return { success: true }; + + return TestConnectionError.ParseResult(new ParseError("Expected object data")); + } + + private getAuthHeaders() { + const username = super.getSecretValue("username"); + const password = super.getSecretValue("password"); + return `Basic ${btoa(`${username}:${password}`)}`; + } + + public async getFirewallVersionAsync(): Promise { + const responseVersion = await fetchWithTrustedCertificatesAsync( + this.url("/api/diagnostics/system/system_information"), + { + headers: { + Authorization: this.getAuthHeaders(), + }, + }, + ); + if (!responseVersion.ok) { + throw new ResponseError(responseVersion); + } + const summary = opnsenseSystemSummarySchema.parse(await responseVersion.json()); + + return { + version: summary.versions.at(0) ?? "Unknown", + }; + } + + private getInterfacesChannel() { + return createChannelEventHistory(`integration:${this.integration.id}:interfaces`, 15); + } + + public async getFirewallInterfacesAsync(): Promise { + const channel = this.getInterfacesChannel(); + + const responseInterfaces = await fetchWithTrustedCertificatesAsync(this.url("/api/diagnostics/traffic/interface"), { + headers: { + Authorization: this.getAuthHeaders(), + }, + }); + + if (!responseInterfaces.ok) { + throw new ResponseError(responseInterfaces); + } + const interfaces = opnsenseInterfacesSchema.parse(await responseInterfaces.json()); + + const returnValue: FirewallInterface[] = []; + const interfaceKeys = Object.keys(interfaces.interfaces); + + for (const key of interfaceKeys) { + const inter = interfaces.interfaces[key]; + if (!inter) continue; + + const bytesTransmitted = inter["bytes transmitted"]; + const bytesReceived = inter["bytes received"]; + const receiveValue = parseInt(bytesReceived, 10); + const transmitValue = parseInt(bytesTransmitted, 10); + + returnValue.push({ + name: inter.name, + receive: receiveValue, + transmit: transmitValue, + }); + } + + await channel.pushAsync(returnValue); + + return await channel.getSliceAsync(0, 1); + } + + public async getFirewallMemoryAsync(): Promise { + const responseMemory = await fetchWithTrustedCertificatesAsync( + this.url("/api/diagnostics/system/systemResources"), + { + headers: { + Authorization: this.getAuthHeaders(), + }, + }, + ); + if (!responseMemory.ok) { + throw new ResponseError(responseMemory); + } + + const memory = opnsenseMemorySchema.parse(await responseMemory.json()); + + // Using parseInt for memoryTotal is normal, the api sends the total memory as a string + const memoryTotal = parseInt(memory.memory.total); + const memoryUsed = memory.memory.used; + const memoryPercent = (100 * memoryUsed) / memoryTotal; + return { + total: memoryTotal, + used: memoryUsed, + percent: memoryPercent, + }; + } + + public async getFirewallCpuAsync(): Promise { + const responseCpu = await fetchWithTrustedCertificatesAsync(this.url("/api/diagnostics/cpu_usage/stream"), { + headers: { + Authorization: this.getAuthHeaders(), + }, + }); + + if (!responseCpu.ok) { + throw new ResponseError(responseCpu); + } + + if (!responseCpu.body) { + throw new Error("ReadableStream not supported in this environment."); + } + + const reader = responseCpu.body.getReader(); + const decoder = new TextDecoder(); + let loopCounter = 0; + try { + while (loopCounter < 10) { + loopCounter++; + const result = await reader.read(); + if (result.done) { + break; + } + if (!(result.value instanceof Uint8Array)) { + throw new Error("Received value is not an Uint8Array."); + } + + const value: AllowSharedBufferSource = result.value; + + const chunk = decoder.decode(value, { stream: true }); + const lines = chunk.split("\n"); + + for (const line of lines) { + if (!line.startsWith("data:")) { + continue; + } + if (loopCounter < 2) { + continue; + } + const data = line.substring(5).trim(); + const cpuValues = opnsenseCPUSchema.parse(JSON.parse(data)); + + return { + ...cpuValues, + }; + } + } + + throw new Error("No valid CPU data found."); + } finally { + await reader.cancel(); + } + } +} diff --git a/packages/integrations/src/opnsense/opnsense-types.ts b/packages/integrations/src/opnsense/opnsense-types.ts new file mode 100644 index 000000000..24d112302 --- /dev/null +++ b/packages/integrations/src/opnsense/opnsense-types.ts @@ -0,0 +1,30 @@ +import { z } from "zod"; + +// API documentation : https://docs.opnsense.org/development/api.html#core-api + +export const opnsenseSystemSummarySchema = z.object({ + name: z.string(), + versions: z.array(z.string()), +}); + +export const opnsenseMemorySchema = z.object({ + memory: z.object({ + total: z.string(), + used: z.number(), + }), +}); + +const interfaceSchema = z.object({ + "bytes received": z.string(), + "bytes transmitted": z.string(), + name: z.string(), +}); + +export const opnsenseInterfacesSchema = z.object({ + interfaces: z.record(interfaceSchema), + time: z.number(), +}); + +export const opnsenseCPUSchema = z.object({ + total: z.number(), +}); diff --git a/packages/integrations/src/quay/quay-integration.ts b/packages/integrations/src/quay/quay-integration.ts new file mode 100644 index 000000000..f84052315 --- /dev/null +++ b/packages/integrations/src/quay/quay-integration.ts @@ -0,0 +1,109 @@ +import type { RequestInit, Response } from "undici"; + +import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { logger } from "@homarr/log"; + +import type { IntegrationTestingInput } from "../base/integration"; +import { Integration } from "../base/integration"; +import { TestConnectionError } from "../base/test-connection/test-connection-error"; +import type { TestingResult } from "../base/test-connection/test-connection-service"; +import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration"; +import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration"; +import type { + ReleaseProviderResponse, + ReleasesRepository, + ReleasesResponse, +} from "../interfaces/releases-providers/releases-providers-types"; +import { releasesResponseSchema } from "./quay-schemas"; + +const localLogger = logger.child({ module: "QuayIntegration" }); + +export class QuayIntegration extends Integration implements ReleasesProviderIntegration { + private async withHeadersAsync(callback: (headers: RequestInit["headers"]) => Promise): Promise { + if (!this.hasSecretValue("personalAccessToken")) return await callback(undefined); + + return await callback({ + Authorization: `token ${this.getSecretValue("personalAccessToken")}`, + }); + } + + protected async testingAsync(input: IntegrationTestingInput): Promise { + const response = await this.withHeadersAsync(async (headers) => { + return await input.fetchAsync(this.url("/api/v1/discovery"), { + headers, + }); + }); + + if (!response.ok) { + return TestConnectionError.StatusResult(response); + } + + return { + success: true, + }; + } + + public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise { + const [owner, name] = repository.identifier.split("/"); + if (!owner || !name) { + localLogger.warn( + `Invalid identifier format. Expected 'owner/name', for ${repository.identifier} with LinuxServerIO integration`, + { + identifier: repository.identifier, + }, + ); + return { + id: repository.id, + error: { code: "invalidIdentifier" }, + }; + } + + const releasesResponse = await this.withHeadersAsync(async (headers) => { + return await fetchWithTrustedCertificatesAsync( + this.url( + `/api/v1/repository/${encodeURIComponent(owner)}/${encodeURIComponent(name)}?includeTags=true&includeStats=true`, + ), + { + headers, + }, + ); + }); + + if (!releasesResponse.ok) { + return { + id: repository.id, + error: { message: releasesResponse.statusText }, + }; + } + + const releasesResponseJson: unknown = await releasesResponse.json(); + const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson); + + if (!success) { + return { + id: repository.id, + error: { + message: error.message, + }, + }; + } else { + const details = { + projectDescription: data.description, + }; + + const releasesProviderResponse = Object.entries(data.tags).reduce((acc, [_, tag]) => { + if (!tag.name || !tag.last_modified) return acc; + + acc.push({ + latestRelease: tag.name, + latestReleaseAt: new Date(tag.last_modified), + releaseUrl: `https://quay.io/repository/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/tag/${encodeURIComponent(tag.name)}`, + }); + + return acc; + }, []); + + return getLatestRelease(releasesProviderResponse, repository, details); + } + } +} diff --git a/packages/integrations/src/quay/quay-schemas.ts b/packages/integrations/src/quay/quay-schemas.ts new file mode 100644 index 000000000..2de28c018 --- /dev/null +++ b/packages/integrations/src/quay/quay-schemas.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +export const releasesResponseSchema = z.object({ + description: z.string().optional(), + tags: z.record( + z.object({ + name: z.string(), + last_modified: z.string(), + }), + ), +}); diff --git a/packages/integrations/src/types.ts b/packages/integrations/src/types.ts index 827c100b3..e38b3e710 100644 --- a/packages/integrations/src/types.ts +++ b/packages/integrations/src/types.ts @@ -1,6 +1,7 @@ export * from "./interfaces/calendar/calendar-types"; export * from "./interfaces/dns-hole-summary/dns-hole-summary-types"; export * from "./interfaces/network-controller-summary/network-controller-summary-types"; +export * from "./interfaces/firewall-summary/firewall-summary-types"; export * from "./interfaces/health-monitoring/health-monitoring-types"; export * from "./interfaces/indexer-manager/indexer-manager-types"; export * from "./interfaces/media-requests/media-request-types"; @@ -8,4 +9,5 @@ export * from "./base/searchable-integration"; export * from "./homeassistant/homeassistant-types"; export * from "./proxmox/proxmox-types"; export * from "./unifi-controller/unifi-controller-types"; +export * from "./opnsense/opnsense-types"; export * from "./interfaces/media-releases"; diff --git a/packages/log/package.json b/packages/log/package.json index e53a7bd05..eab4666f6 100644 --- a/packages/log/package.json +++ b/packages/log/package.json @@ -33,7 +33,7 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/modals-collection/package.json b/packages/modals-collection/package.json index 13a2e37d3..29d3a1601 100644 --- a/packages/modals-collection/package.json +++ b/packages/modals-collection/package.json @@ -33,19 +33,19 @@ "@homarr/translation": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", - "@mantine/core": "^8.2.1", + "@mantine/core": "^8.2.2", "@tabler/icons-react": "^3.34.1", "dayjs": "^1.11.13", - "next": "15.4.4", - "react": "19.1.0", - "react-dom": "19.1.0", + "next": "15.4.5", + "react": "19.1.1", + "react-dom": "19.1.1", "zod": "^3.25.76" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/modals/package.json b/packages/modals/package.json index 800ef7b61..be12e2680 100644 --- a/packages/modals/package.json +++ b/packages/modals/package.json @@ -24,15 +24,15 @@ "dependencies": { "@homarr/translation": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0", - "@mantine/core": "^8.2.1", - "@mantine/hooks": "^8.2.1", - "react": "19.1.0" + "@mantine/core": "^8.2.2", + "@mantine/hooks": "^8.2.2", + "react": "19.1.1" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/notifications/package.json b/packages/notifications/package.json index 9d9f7f5ad..6073830ce 100644 --- a/packages/notifications/package.json +++ b/packages/notifications/package.json @@ -24,14 +24,14 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/ui": "workspace:^0.1.0", - "@mantine/notifications": "^8.2.1", + "@mantine/notifications": "^8.2.2", "@tabler/icons-react": "^3.34.1" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/old-import/package.json b/packages/old-import/package.json index 046341c08..d49375618 100644 --- a/packages/old-import/package.json +++ b/packages/old-import/package.json @@ -37,12 +37,12 @@ "@homarr/translation": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", - "@mantine/core": "^8.2.1", - "@mantine/hooks": "^8.2.1", + "@mantine/core": "^8.2.2", + "@mantine/hooks": "^8.2.2", "adm-zip": "0.5.16", - "next": "15.4.4", - "react": "19.1.0", - "react-dom": "19.1.0", + "next": "15.4.5", + "react": "19.1.1", + "react-dom": "19.1.1", "superjson": "2.2.2", "zod": "^3.25.76", "zod-form-data": "^2.0.7" @@ -52,7 +52,7 @@ "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "@types/adm-zip": "0.5.7", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/old-schema/package.json b/packages/old-schema/package.json index e1bb0dfb3..a4c04b729 100644 --- a/packages/old-schema/package.json +++ b/packages/old-schema/package.json @@ -29,7 +29,7 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/ping/eslint.config.js b/packages/ping/eslint.config.js new file mode 100644 index 000000000..f7a5a7d36 --- /dev/null +++ b/packages/ping/eslint.config.js @@ -0,0 +1,4 @@ +import baseConfig from "@homarr/eslint-config/base"; + +/** @type {import('typescript-eslint').Config} */ +export default [...baseConfig]; diff --git a/packages/ping/index.ts b/packages/ping/index.ts new file mode 100644 index 000000000..3bd16e178 --- /dev/null +++ b/packages/ping/index.ts @@ -0,0 +1 @@ +export * from "./src"; diff --git a/packages/ping/package.json b/packages/ping/package.json new file mode 100644 index 000000000..4c046adbe --- /dev/null +++ b/packages/ping/package.json @@ -0,0 +1,36 @@ +{ + "name": "@homarr/ping", + "version": "0.1.0", + "private": true, + "license": "Apache-2.0", + "type": "module", + "exports": { + ".": "./index.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/certificates": "workspace:^0.1.0", + "@homarr/common": "workspace:^0.1.0", + "@homarr/log": "workspace:^0.1.0" + }, + "devDependencies": { + "@homarr/eslint-config": "workspace:^0.2.0", + "@homarr/prettier-config": "workspace:^0.1.0", + "@homarr/tsconfig": "workspace:^0.1.0", + "eslint": "^9.32.0", + "typescript": "^5.9.2" + } +} diff --git a/packages/ping/src/index.ts b/packages/ping/src/index.ts new file mode 100644 index 000000000..1bf624706 --- /dev/null +++ b/packages/ping/src/index.ts @@ -0,0 +1,37 @@ +import { fetch } from "undici"; + +import { extractErrorMessage } from "@homarr/common"; +import { LoggingAgent } from "@homarr/common/server"; +import { logger } from "@homarr/log"; + +export const sendPingRequestAsync = async (url: string) => { + try { + const controller = new AbortController(); + + // 10 seconds timeout: + const timeoutId = setTimeout(() => controller.abort(), 10000); + const start = performance.now(); + + return await fetch(url, { + dispatcher: new LoggingAgent({ + connect: { + rejectUnauthorized: false, // Ping should always work, even with untrusted certificates + }, + }), + signal: controller.signal, + }) + .finally(() => { + clearTimeout(timeoutId); + }) + .then((response) => { + const end = performance.now(); + const durationMs = end - start; + return { statusCode: response.status, durationMs }; + }); + } catch (error) { + logger.error(new Error(`Failed to send ping request to "${url}"`, { cause: error })); + return { + error: extractErrorMessage(error), + }; + } +}; diff --git a/packages/ping/tsconfig.json b/packages/ping/tsconfig.json new file mode 100644 index 000000000..612bef8df --- /dev/null +++ b/packages/ping/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@homarr/tsconfig/base.json", + "compilerOptions": { + "types": ["node"], + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + }, + "include": ["*.ts", "src"], + "exclude": ["node_modules"] +} diff --git a/packages/redis/package.json b/packages/redis/package.json index 37624a09b..99d695f9b 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -27,14 +27,14 @@ "@homarr/db": "workspace:^", "@homarr/definitions": "workspace:^", "@homarr/log": "workspace:^", - "ioredis": "5.6.1", + "ioredis": "5.7.0", "superjson": "2.2.2" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/redis/src/index.ts b/packages/redis/src/index.ts index 918fbee09..05d4389f9 100644 --- a/packages/redis/src/index.ts +++ b/packages/redis/src/index.ts @@ -1,4 +1,4 @@ -import { LogLevel } from "@homarr/log/constants"; +import type { LogLevel } from "@homarr/log/constants"; import { createListChannel, createQueueChannel, createSubPubChannel } from "./lib/channel"; @@ -9,11 +9,16 @@ export { createIntegrationOptionsChannel, createWidgetOptionsChannel, createChannelWithLatestAndEvents, + createChannelEventHistory, handshakeAsync, createSubPubChannel, createGetSetChannel, } from "./lib/channel"; +export const exampleChannel = createSubPubChannel<{ message: string }>("example"); +export const pingChannel = createSubPubChannel< + { url: string; statusCode: number; durationMs: number } | { url: string; error: string } +>("ping"); export const pingUrlChannel = createListChannel("ping-url"); export const homeAssistantEntityState = createSubPubChannel<{ diff --git a/packages/redis/src/lib/channel.ts b/packages/redis/src/lib/channel.ts index d649563ed..5a184e55a 100644 --- a/packages/redis/src/lib/channel.ts +++ b/packages/redis/src/lib/channel.ts @@ -232,7 +232,7 @@ export const createChannelEventHistory = (channelName: string, maxElement if (length <= maxElements) { return; } - await getSetClient.ltrim(channelName, length - maxElements, length); + await getSetClient.ltrim(channelName, 0, maxElements - 1); }; return { diff --git a/packages/request-handler/package.json b/packages/request-handler/package.json index 5a3435c1a..48f3b7de9 100644 --- a/packages/request-handler/package.json +++ b/packages/request-handler/package.json @@ -32,13 +32,13 @@ "dayjs": "^1.11.13", "octokit": "^5.0.3", "superjson": "2.2.2", - "undici": "7.12.0" + "undici": "7.13.0" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/request-handler/src/firewall.ts b/packages/request-handler/src/firewall.ts new file mode 100644 index 000000000..3754f1099 --- /dev/null +++ b/packages/request-handler/src/firewall.ts @@ -0,0 +1,64 @@ +import dayjs from "dayjs"; + +import type { IntegrationKindByCategory } from "@homarr/definitions"; +import { createIntegrationAsync } from "@homarr/integrations"; +import type { + FirewallCpuSummary, + FirewallInterfacesSummary, + FirewallMemorySummary, + FirewallVersionSummary, +} from "@homarr/integrations/types"; + +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; + +export const firewallCpuRequestHandler = createCachedIntegrationRequestHandler< + FirewallCpuSummary, + IntegrationKindByCategory<"firewall">, + Record +>({ + async requestAsync(integration, _input) { + const integrationInstance = await createIntegrationAsync(integration); + return integrationInstance.getFirewallCpuAsync(); + }, + cacheDuration: dayjs.duration(5, "seconds"), + queryKey: "firewallCpuSummary", +}); + +export const firewallMemoryRequestHandler = createCachedIntegrationRequestHandler< + FirewallMemorySummary, + IntegrationKindByCategory<"firewall">, + Record +>({ + async requestAsync(integration, _input) { + const integrationInstance = await createIntegrationAsync(integration); + return await integrationInstance.getFirewallMemoryAsync(); + }, + cacheDuration: dayjs.duration(15, "seconds"), + queryKey: "firewallMemorySummary", +}); + +export const firewallInterfacesRequestHandler = createCachedIntegrationRequestHandler< + FirewallInterfacesSummary[], + IntegrationKindByCategory<"firewall">, + Record +>({ + async requestAsync(integration, _input) { + const integrationInstance = await createIntegrationAsync(integration); + return await integrationInstance.getFirewallInterfacesAsync(); + }, + cacheDuration: dayjs.duration(30, "seconds"), + queryKey: "firewallInterfacesSummary", +}); + +export const firewallVersionRequestHandler = createCachedIntegrationRequestHandler< + FirewallVersionSummary, + IntegrationKindByCategory<"firewall">, + Record +>({ + async requestAsync(integration, _input) { + const integrationInstance = await createIntegrationAsync(integration); + return await integrationInstance.getFirewallVersionAsync(); + }, + cacheDuration: dayjs.duration(1, "hour"), + queryKey: "firewallVersionSummary", +}); diff --git a/packages/request-handler/src/ping.ts b/packages/request-handler/src/ping.ts deleted file mode 100644 index a53930b2c..000000000 --- a/packages/request-handler/src/ping.ts +++ /dev/null @@ -1,50 +0,0 @@ -import dayjs from "dayjs"; -import duration from "dayjs/plugin/duration"; -import { fetch } from "undici"; - -import { extractErrorMessage } from "@homarr/common"; -import { LoggingAgent } from "@homarr/common/server"; -import { logger } from "@homarr/log"; - -import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; - -dayjs.extend(duration); - -type PingResponse = - | { - statusCode: number; - durationMs: number; - } - | { - error: string; - }; -export const pingRequestHandler = createCachedWidgetRequestHandler({ - queryKey: "pingResult", - widgetKind: "app", - async requestAsync(input) { - return await sendPingRequestAsync(input.url); - }, - cacheDuration: dayjs.duration(1, "minute"), -}); - -const sendPingRequestAsync = async (url: string) => { - try { - const start = performance.now(); - return await fetch(url, { - dispatcher: new LoggingAgent({ - connect: { - rejectUnauthorized: false, - }, - }), - }).then((response) => { - const end = performance.now(); - logger.debug(`Ping request succeeded url="${url}" status="${response.status}" duration="${end - start}ms"`); - return { statusCode: response.status, durationMs: end - start }; - }); - } catch (error) { - logger.error(new Error(`Failed to send ping request to url="${url}"`, { cause: error })); - return { - error: extractErrorMessage(error), - }; - } -}; diff --git a/packages/server-settings/package.json b/packages/server-settings/package.json index 0e5c2c411..a15729112 100644 --- a/packages/server-settings/package.json +++ b/packages/server-settings/package.json @@ -29,7 +29,7 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/settings/package.json b/packages/settings/package.json index 93539369b..c382e1681 100644 --- a/packages/settings/package.json +++ b/packages/settings/package.json @@ -26,16 +26,16 @@ "@homarr/api": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0", - "@mantine/dates": "^8.2.1", - "next": "15.4.4", - "react": "19.1.0", - "react-dom": "19.1.0" + "@mantine/dates": "^8.2.2", + "next": "15.4.5", + "react": "19.1.1", + "react-dom": "19.1.1" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/spotlight/package.json b/packages/spotlight/package.json index db5fe3d56..dd8df1e08 100644 --- a/packages/spotlight/package.json +++ b/packages/spotlight/package.json @@ -33,21 +33,21 @@ "@homarr/settings": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0", - "@mantine/core": "^8.2.1", - "@mantine/hooks": "^8.2.1", - "@mantine/spotlight": "^8.2.1", + "@mantine/core": "^8.2.2", + "@mantine/hooks": "^8.2.2", + "@mantine/spotlight": "^8.2.2", "@tabler/icons-react": "^3.34.1", "jotai": "^2.12.5", - "next": "15.4.4", - "react": "19.1.0", - "react-dom": "19.1.0", + "next": "15.4.5", + "react": "19.1.1", + "react-dom": "19.1.1", "use-deep-compare-effect": "^1.8.1" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/translation/package.json b/packages/translation/package.json index c7e91366a..78c437b26 100644 --- a/packages/translation/package.json +++ b/packages/translation/package.json @@ -32,16 +32,16 @@ "dayjs": "^1.11.13", "deepmerge": "4.3.1", "mantine-react-table": "2.0.0-beta.9", - "next": "15.4.4", + "next": "15.4.5", "next-intl": "4.3.4", - "react": "19.1.0", - "react-dom": "19.1.0" + "react": "19.1.1", + "react-dom": "19.1.1" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/translation/src/lang/ca.json b/packages/translation/src/lang/ca.json index 261e2f349..a45144425 100644 --- a/packages/translation/src/lang/ca.json +++ b/packages/translation/src/lang/ca.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "", "description": "", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/cn.json b/packages/translation/src/lang/cn.json index 9b03f68e7..dc1679d21 100644 --- a/packages/translation/src/lang/cn.json +++ b/packages/translation/src/lang/cn.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "全局比率" }, + "mediaReleases": { + "name": "媒体发布", + "description": "显示来自不同集成的新添加介质或即将发布的版本", + "option": { + "layout": { + "label": "布局", + "option": { + "backdrop": { + "label": "背景" + }, + "poster": { + "label": "海报" + } + } + }, + "showDescriptionTooltip": { + "label": "显示描述提示" + }, + "showType": { + "label": "显示媒体类型徽章" + }, + "showSource": { + "label": "显示来源集成" + } + }, + "length": { + "duration": "{length} 分钟" + } + }, "mediaRequests-requestList": { "name": "媒体请求列表", "description": "查看 Overr 或 Jellyseerr 实例中的所有媒体请求列表", @@ -2309,7 +2338,7 @@ "openProjectPage": "打开项目页面", "openReleasePage": "打开发布页面", "releaseDescription": "发布说明", - "projectDescription": "", + "projectDescription": "项目描述", "created": "已创建", "error": { "label": "错误", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "调试", + "info": "信息", + "warn": "警告", + "error": "错误" + } + } } } diff --git a/packages/translation/src/lang/cs.json b/packages/translation/src/lang/cs.json index 6f9d2339a..b718364a2 100644 --- a/packages/translation/src/lang/cs.json +++ b/packages/translation/src/lang/cs.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "", "description": "Podívejte se na seznam všech požadavků na média z vaší instance Overseerr nebo Jellyseerr", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/da.json b/packages/translation/src/lang/da.json index f0a1756b5..a83c9f157 100644 --- a/packages/translation/src/lang/da.json +++ b/packages/translation/src/lang/da.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "Globalt Forhold" }, + "mediaReleases": { + "name": "Medieudgivelser", + "description": "Vis nyligt tilføjede medier eller kommende udgivelser fra forskellige integrationer", + "option": { + "layout": { + "label": "Layout", + "option": { + "backdrop": { + "label": "Baggrund" + }, + "poster": { + "label": "Plakat" + } + } + }, + "showDescriptionTooltip": { + "label": "Vis beskrivelsesværktøjstip" + }, + "showType": { + "label": "Vis medietype badge" + }, + "showSource": { + "label": "Vis kildeintegration" + } + }, + "length": { + "duration": "{length}min" + } + }, "mediaRequests-requestList": { "name": "Medie Forespørgsler Liste", "description": "Se en liste over alle medieforespørgsler fra din Overseerr eller Jellyseerr instans", @@ -2309,7 +2338,7 @@ "openProjectPage": "Åbn Projektside", "openReleasePage": "Åbn Udgivelsesside", "releaseDescription": "Udgivelse Beskrivelse", - "projectDescription": "", + "projectDescription": "Projektbeskrivelse", "created": "Oprettet", "error": { "label": "Fejl", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "Fejlsøg", + "info": "Info", + "warn": "Advar", + "error": "Fejl" + } + } } } diff --git a/packages/translation/src/lang/de-CH.json b/packages/translation/src/lang/de-CH.json index a9d507c14..884167540 100644 --- a/packages/translation/src/lang/de-CH.json +++ b/packages/translation/src/lang/de-CH.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "Globales Verhältnis" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "Liste der Medienanfragen", "description": "Sehen Sie eine Liste aller Medienanfragen von Ihrer Overseerr- oder Jellyseerr-Instanz", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/de.json b/packages/translation/src/lang/de.json index f272806f9..38e611da2 100644 --- a/packages/translation/src/lang/de.json +++ b/packages/translation/src/lang/de.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "Globales Verhältnis" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "Liste der Medienanfragen", "description": "Sehen Sie eine Liste aller Medienanfragen von Ihrer Overseerr- oder Jellyseerr-Instanz", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/el.json b/packages/translation/src/lang/el.json index 5f3769fb3..123edbd13 100644 --- a/packages/translation/src/lang/el.json +++ b/packages/translation/src/lang/el.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "", "description": "Δείτε μια λίστα με όλα τα αιτήματα μέσων ενημέρωσης από την περίπτωση Overseerr ή Jellyseerr", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/en-gb.json b/packages/translation/src/lang/en-gb.json index cbf2dfe1e..b938098b3 100644 --- a/packages/translation/src/lang/en-gb.json +++ b/packages/translation/src/lang/en-gb.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "", "description": "", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 5df3d35d6..af6198cd0 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -2257,6 +2257,9 @@ "showDetails": { "label": "Show Details" }, + "showOnlyIcon": { + "label": "Show Only Icon" + }, "topReleases": { "label": "Top Releases", "description": "The max number of latest releases to show. Zero means no limit." @@ -2273,7 +2276,9 @@ "listFoundImages": "List of found images", "listAlreadyImportedImages": "List of already imported images", "allImagesAlreadyImported": "All images already imported", - "onlyAdminCanImport": "Only administrators can import from docker" + "onlyAdminCanImport": "Only administrators can import from docker", + "selectAll": "Select all", + "deselectAll": "Deselect all" }, "provider": { "label": "Provider" @@ -2335,6 +2340,7 @@ "starsCount": "Stars", "forksCount": "Forks", "issuesCount": "Open Issues", + "markViewed": "Mark as viewed", "openProjectPage": "Open Project Page", "openReleasePage": "Open Release Page", "releaseDescription": "Release Description", @@ -2407,6 +2413,35 @@ "internalServerError": "Failed to fetch Network Controller Summary" } }, + "firewall": { + "name": "Firewall Monitoring", + "description": "Displays a summary of firewalls", + "tab": { + "system": "System", + "interfaces": "Interfaces" + }, + "error": { + "internalServerError": "Unable to get data from firewall" + }, + "option": { + "interfaces": "Network interfaces to display" + }, + "widget": { + "fwname": "Name", + "version": "Version", + "versiontitle": "Versions", + "cputitle": "CPU usage", + "memorytitle": "Memory usage", + "cpu": "CPU", + "memory": "Memory", + "interfaces": { + "name": "name", + "trans": "Transmited", + "recv": "Received", + "title": "Network Interfaces" + } + } + }, "notifications": { "name": "Notifications", "description": "Display notification history from an integration", @@ -3186,6 +3221,18 @@ }, "dockerContainers": { "label": "Docker containers" + }, + "firewallCpu": { + "label": "Firewall CPU" + }, + "firewallMemory": { + "label": "Firewall Memory" + }, + "firewallVersion": { + "label": "Firewall Version" + }, + "firewallInterfaces": { + "label": "Firewall Interfaces" } }, "interval": { diff --git a/packages/translation/src/lang/es.json b/packages/translation/src/lang/es.json index 0b12db5f7..e35f5e4a7 100644 --- a/packages/translation/src/lang/es.json +++ b/packages/translation/src/lang/es.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "", "description": "Mostrar una lista de todas las solicitudes multimedia de tu instancia de Overseerr o Jellyseerr", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/et.json b/packages/translation/src/lang/et.json index d138f8648..db2c5ca85 100644 --- a/packages/translation/src/lang/et.json +++ b/packages/translation/src/lang/et.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "", "description": "", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/fr.json b/packages/translation/src/lang/fr.json index b71665751..59263e0a2 100644 --- a/packages/translation/src/lang/fr.json +++ b/packages/translation/src/lang/fr.json @@ -620,7 +620,7 @@ "create": { "title": "Créer une nouvelle application", "description": "Créer une nouvelle application ", - "action": "" + "action": "Ouvrir la création de l'application" }, "add": "Ajouter une application" } @@ -705,125 +705,125 @@ "error": { "common": { "cause": { - "title": "" + "title": "Cause avec plus de détails" } }, "unknown": { - "title": "", - "description": "" + "title": "Erreur inconnue", + "description": "Une erreur inconnue s'est produite, ouvrez la cause ci-dessous pour voir plus de détails" }, "parse": { - "title": "", - "description": "" + "title": "Erreur d'analyse", + "description": "La réponse n'a pas pu être analysée. Veuillez vérifier que l'URL pointe vers l'URL de base du service." }, "authorization": { "title": "", - "description": "" + "description": "La requête n'a pas été autorisée. Veuillez vérifier que les informations d'identification sont correctes et que vous les avez configurées avec suffisamment d'autorisations." }, "statusCode": { - "title": "", - "description": "", - "otherDescription": "", + "title": "Erreur de réponse", + "description": "Réponse {statusCode} ({reason}) inattendue de . Veuillez vérifier que l'URL pointe vers l'URL de base de l'intégration.", + "otherDescription": "Réponse {statusCode} inattendue de reçue. Veuillez vérifier que l'URL pointe vers l'URL de base de l'intégration.", "reason": { - "badRequest": "", - "notFound": "", - "tooManyRequests": "", - "internalServerError": "", - "serviceUnavailable": "", - "gatewayTimeout": "" + "badRequest": "Requête incorrecte", + "notFound": "Non trouvé", + "tooManyRequests": "Trop de requêtes", + "internalServerError": "Erreur interne du serveur", + "serviceUnavailable": "Service indisponible", + "gatewayTimeout": "Délai d'attente de la passerelle dépassé" } }, "certificate": { - "title": "", + "title": "Erreur de certificat", "description": { - "expired": "", - "notYetValid": "", - "untrusted": "", - "hostnameMismatch": "" + "expired": "Le certificat a expiré.", + "notYetValid": "Le certificat n'est pas encore valide.", + "untrusted": "Le certificat n'est pas digne de confiance.", + "hostnameMismatch": "Le nom d'hôte du certificat ne correspond pas à l'URL." }, "alert": { "permission": { - "title": "", - "message": "" + "title": "Permissions insuffisantes", + "message": "Vous n'êtes pas autorisé à faire confiance ou télécharger des certificats. Veuillez contacter votre administrateur pour télécharger le certificat racine nécessaire." }, "hostnameMismatch": { "title": "Nom d'hôte incohérent", - "message": "" + "message": "Le nom d'hôte dans le certificat ne correspond pas au nom d'hôte auquel vous vous connectez. Cela pourrait indiquer un risque de sécurité, mais vous pouvez quand même choisir de faire confiance à ce certificat." }, "extract": { - "title": "", - "message": "" + "title": "Échec de l'extraction de l'autorité de certification", + "message": "Seuls les certificats auto-signés sans chaîne peuvent être récupérés automatiquement. Si vous utilisez un certificat auto-signé, assurez-vous de télécharger le certificat CA manuellement. Vous pouvez trouver des instructions sur la façon de faire ceci ." } }, "action": { "retry": { - "label": "" + "label": "Réessayer la création" }, "trust": { - "label": "" + "label": "Faire confiance au certificat" }, "upload": { - "label": "" + "label": "Télécharger le certificat" } }, "hostnameMismatch": { "confirm": { - "title": "", + "title": "Faire confiant à l'incohérence du nom d'hôte", "message": "" }, "notification": { "success": { - "title": "", - "message": "" + "title": "Certificat approuvé", + "message": "Nom d'hôte ajouté à la liste de certificats de confiance" }, "error": { - "title": "", - "message": "" + "title": "Impossible de faire confiance au certificat", + "message": "Le certificat avec un nom d'hôte incohérent n'a pas pu être approuvé" } } }, "selfSigned": { "confirm": { - "title": "", - "message": "" + "title": "Faire confiance au certificat auto-signé", + "message": "Êtes-vous sûr de vouloir faire confiance à ce certificat auto-signé ?" }, "notification": { "success": { - "title": "", - "message": "" + "title": "Certificat approuvé", + "message": "Certificat ajouté à la liste des certificats de confiance" }, "error": { - "title": "", - "message": "" + "title": "Impossible de faire confiance au certificat", + "message": "Impossible d'ajouter le certificat à la liste des certificats de confiance" } } }, "details": { - "title": "", - "description": "", + "title": "Détails", + "description": "Examinez les informations sur le certificat avant de décider de lui faire confiance.", "content": { - "action": "", - "title": "" + "action": "Afficher le contenu", + "title": "Certificat PEM" } } }, "request": { - "title": "", + "title": "Erreur dans la requête", "description": { "connection": { - "hostUnreachable": "", - "networkUnreachable": "", - "refused": "", - "reset": "" + "hostUnreachable": "Le serveur n'a pas pu être atteint. Cela signifie généralement que l'hôte est hors ligne ou injoignable depuis votre réseau.", + "networkUnreachable": "Le réseau est inaccessible. Veuillez vérifier votre connexion internet ou la configuration du réseau.", + "refused": "Le serveur a refusé la connexion. Il n'est peut-être pas en cours d'exécution ou rejette les requêtes sur le port spécifié.", + "reset": "La connexion a été fermée de façon inattendue par le serveur. Cela peut se produire si le serveur est instable ou redémarre." }, "dns": { - "notFound": "", - "timeout": "", - "noAnswer": "" + "notFound": "L'adresse du serveur est introuvable. Veuillez vérifier l'URL pour les fautes de frappe ou les noms de domaine non valides.", + "timeout": "La recherche DNS a expiré. Il peut s'agir d'un problème temporaire, veuillez réessayer dans quelques instants.", + "noAnswer": "Le serveur DNS n'a pas renvoyé de réponse valide. Le domaine peut exister mais n'a pas d'enregistrements valides." }, "timeout": { - "aborted": "", - "timeout": "" + "aborted": "La requête a été annulée avant qu'elle ne puisse être terminée. Cela peut être dû à une action de l'utilisateur ou à un délai d'expiration du système.", + "timeout": "La requête a pris trop de temps à être terminée et a été expirée. Vérifiez votre réseau ou réessayez plus tard." } } } @@ -896,7 +896,7 @@ }, "tooManyRequests": { "title": "Trop de requêtes en un temps donné", - "message": "" + "message": "Il y a eu trop de requêtes. Vous avez probablement été limité ou rejeté par le système cible" } } }, @@ -938,12 +938,12 @@ "newLabel": "Nouveau domaine" }, "personalAccessToken": { - "label": "", - "newLabel": "" + "label": "Jeton d'accès personnel", + "newLabel": "Nouveau jeton d'accès personnel" }, "topic": { - "label": "", - "newLabel": "" + "label": "Sujet", + "newLabel": "Nouveau sujet" } } }, @@ -1012,7 +1012,7 @@ "cancel": "Annuler", "delete": "Supprimer", "discard": "Abandonner", - "close": "", + "close": "Fermer", "confirm": "Confirmer", "continue": "Continuer", "previous": "Précédent", @@ -1507,7 +1507,7 @@ "width": "Largeur", "height": "Hauteur" }, - "placeholder": "" + "placeholder": "Commencer à écrire vos notes" }, "iframe": { "name": "iFrame", @@ -1585,10 +1585,10 @@ "description": "Affiche le cours des actions d'une entreprise", "option": { "stock": { - "label": "" + "label": "Symbole de l'action" }, "timeRange": { - "label": "", + "label": "Intervalle de temps", "option": { "1d": { "label": "1 jour" @@ -1767,7 +1767,7 @@ "label": "Afficher les infos de la mémoire" }, "showUptime": { - "label": "" + "label": "Afficher le temps de disponibilité" }, "fileSystem": { "label": "Afficher les infos sur le système de fichiers" @@ -1776,7 +1776,7 @@ "label": "Onglet par défaut" }, "visibleClusterSections": { - "label": "" + "label": "Sections visibles de cluster" }, "sectionIndicatorRequirement": { "label": "Exigence de l'indicateur de section" @@ -1848,11 +1848,11 @@ } }, "dockerContainers": { - "name": "", - "description": "", + "name": "Statistiques de Docker", + "description": "Statistiques de vos conteneurs (Ce widget ne peut être ajouté qu'avec les privilèges d'administrateur)", "option": {}, "error": { - "internalServerError": "" + "internalServerError": "Impossible de récupérer les statistiques des conteneurs" } }, "common": { @@ -1955,7 +1955,7 @@ "label": "Afficher les entrées Torrent marquées comme terminées" }, "showCompletedHttp": { - "label": "" + "label": "Afficher les entrées diverses marquées comme terminées" }, "activeTorrentThreshold": { "label": "Masquer les Torrents terminés sous ce seuil (en kiB/s)" @@ -1970,8 +1970,8 @@ "label": "Utiliser le filtre pour calculer le ratio" }, "limitPerIntegration": { - "label": "", - "description": "" + "label": "Limiter les éléments par intégration", + "description": "Cela limitera le nombre d'éléments affichés par intégration, pas globalement" } }, "errors": { @@ -2051,10 +2051,10 @@ "completed": "Complété", "failed": "Échec", "processing": "Traitement en cours", - "leeching": "", - "stalled": "", + "leeching": "En téléchargement", + "stalled": "Bloqué", "unknown": "Inconnu", - "seeding": "" + "seeding": "En partage" }, "actions": { "clients": { @@ -2079,6 +2079,35 @@ }, "globalRatio": "Ratio global" }, + "mediaReleases": { + "name": "Sorties récentes", + "description": "Afficher les médias récemment ajoutés ou les publications à venir de différentes intégrations", + "option": { + "layout": { + "label": "Mise en page", + "option": { + "backdrop": { + "label": "Arrière-plan" + }, + "poster": { + "label": "Affiche" + } + } + }, + "showDescriptionTooltip": { + "label": "Afficher l'info-bulle de description" + }, + "showType": { + "label": "Afficher le badge du type de média" + }, + "showSource": { + "label": "Afficher l'intégration de la source" + } + }, + "length": { + "duration": "{length}min" + } + }, "mediaRequests-requestList": { "name": "Liste des demandes de médias", "description": "Voir la liste de toutes les demandes de médias de votre instance Overseerr ou Jellyseerr", @@ -2098,15 +2127,15 @@ "processing": "Traitement en cours", "partiallyAvailable": "Partiel", "available": "Disponible", - "blacklisted": "", - "deleted": "" + "blacklisted": "Sur la liste noire", + "deleted": "Supprimé" }, "status": { "pending": "En attente", "approved": "Approuvé", "declined": "Refusé", "failed": "Échec", - "completed": "" + "completed": "Complété" }, "toBeDetermined": "À déterminer" }, @@ -2210,115 +2239,115 @@ } }, "releases": { - "name": "", - "description": "", + "name": "Versions", + "description": "Affiche une liste de la version courante des référentiels donnés avec la version regex donnée.", "option": { "newReleaseWithin": { - "label": "", - "description": "" + "label": "Nouvelle version dans", + "description": "Exemple d'utilisation : 1w (1 semaine), 10M (10 mois). Type d'unité acceptée h (heures), d (jours), w (semaines), M (mois), y (années). Laisser vide pour ne pas mettre en évidence les nouvelles versions." }, "staleReleaseWithin": { - "label": "", - "description": "" + "label": "Version obsolète dans", + "description": "Exemple d'utilisation : 1w (1 semaine), 10M (10 mois). Type d'unité acceptée h (heures), d (jours), w (semaines), M (mois), y (années). Laisser vide pour ne pas mettre en évidence les versions obsolètes." }, "showOnlyHighlighted": { - "label": "", - "description": "" + "label": "Afficher uniquement les surbrillances", + "description": "Afficher uniquement les nouvelles versions ou les versions obsolètes. Comme pour les versions ci-dessus." }, "showDetails": { - "label": "" + "label": "Afficher les détails" }, "topReleases": { - "label": "", - "description": "" + "label": "Meilleures sorties", + "description": "Le nombre maximum de dernières versions à afficher. Zéro signifie aucune limite." }, "repositories": { - "label": "", + "label": "Dépôts", "addRepository": { - "label": "" + "label": "Ajouter un dépôt" }, "importRepositories": { - "label": "", - "loading": "", - "noImagesFound": "", - "listFoundImages": "", - "listAlreadyImportedImages": "", - "allImagesAlreadyImported": "", - "onlyAdminCanImport": "" + "label": "Importer depuis docker", + "loading": "Chargement des images docker", + "noImagesFound": "Aucune image docker trouvée", + "listFoundImages": "Liste des images trouvées", + "listAlreadyImportedImages": "Liste des images déjà importées", + "allImagesAlreadyImported": "Toutes les images déjà importées", + "onlyAdminCanImport": "Seuls les administrateurs peuvent importer depuis docker" }, "provider": { - "label": "" + "label": "Fournisseur" }, "identifier": { - "label": "", - "placeholder": "" + "label": "Identifiant", + "placeholder": "Nom ou Propriétaire / Nom" }, "name": { - "label": "" + "label": "Nom" }, "versionFilter": { - "label": "", + "label": "Filtre de versions", "prefix": { - "label": "" + "label": "Préfixe" }, "precision": { - "label": "", + "label": "Précision", "options": { - "none": "" + "none": "Aucun" } }, "suffix": { - "label": "" + "label": "Suffixe" }, "regex": { - "label": "" + "label": "Expression Régulière" } }, "edit": { - "label": "" + "label": "Modifier" }, "editForm": { - "title": "", + "title": "Modifier le dépôt", "cancel": { - "label": "" + "label": "Annuler" }, "confirm": { - "label": "" + "label": "Confirmer" } }, "importForm": { - "title": "" + "title": "Importer depuis Docker" }, "example": { - "label": "" + "label": "Exemple" }, - "invalid": "", + "invalid": "Définition de dépôt invalide, veuillez vérifier les valeurs", "noProvider": { - "label": "", - "tooltip": "" + "label": "Aucun fournisseur", + "tooltip": "Le fournisseur n'a pas pu être analysé, veuillez le définir manuellement après l'importation des images" } } }, - "not-found": "", - "pre-release": "", - "archived": "", + "not-found": "Non trouvé", + "pre-release": "Pré-publication", + "archived": "Archivé", "forked": "", - "starsCount": "", + "starsCount": "Étoiles", "forksCount": "", - "issuesCount": "", - "openProjectPage": "", - "openReleasePage": "", - "releaseDescription": "", - "projectDescription": "", - "created": "", + "issuesCount": "Problèmes ouverts", + "openProjectPage": "Ouvrir la page du projet", + "openReleasePage": "Ouvrir la page de publication", + "releaseDescription": "Description de la publication", + "projectDescription": "Description du projet", + "created": "Créé le", "error": { - "label": "", + "label": "Erreur", "messages": { - "invalidIdentifier": "", - "noMatchingVersion": "", - "noReleasesFound": "", - "noProviderSeleceted": "", - "noProviderResponse": "" + "invalidIdentifier": "Identifiant non valide", + "noMatchingVersion": "Aucune version correspondante trouvée", + "noReleasesFound": "Aucune publication trouvée", + "noProviderSeleceted": "Aucun fournisseur sélectionné", + "noProviderResponse": "Aucune réponse du fournisseur" } } }, @@ -2326,62 +2355,62 @@ "option": {}, "card": { "vpn": { - "countConnected": "" + "countConnected": "{count} connecté" } }, "error": { - "integrationsDisconnected": "", - "unknownContentOption": "" + "integrationsDisconnected": "Aucune donnée disponible, toutes les intégrations sont déconnectées", + "unknownContentOption": "Option de contenu inconnue pour le widget de résumé du contrôleur réseau : " }, - "name": "", - "description": "" + "name": "Résumé du contrôleur réseau", + "description": "Affiche le résumé d'un contrôleur réseau (comme le contrôleur UniFi)" }, "networkControllerStatus": { "card": { "variants": { "wired": { - "name": "" + "name": "Filaire" }, "wifi": { - "name": "" + "name": "Wi-Fi" } }, "users": { - "label": "" + "label": "Utilisateurs" }, "guests": { - "label": "" + "label": "Invités" } }, "option": { "content": { "option": { "wifi": { - "label": "" + "label": "Wi-Fi" }, "wired": { - "label": "" + "label": "Filaire" } }, - "label": "" + "label": "Contenu du Widget" } }, "error": { - "integrationsDisconnected": "", - "unknownContentOption": "" + "integrationsDisconnected": "Aucune donnée disponible, toutes les intégrations sont déconnectées", + "unknownContentOption": "Option de contenu inconnue pour le widget d'état du réseau: " }, - "name": "", - "description": "" + "name": "État du réseau", + "description": "Afficher les périphériques connectés sur un réseau" }, "networkController": { "error": { - "internalServerError": "" + "internalServerError": "Impossible de récupérer le résumé du contrôleur réseau" } }, "notifications": { - "name": "", - "description": "", - "noItems": "", + "name": "Notifications", + "description": "Afficher l'historique des notifications à partir d'une intégration", + "noItems": "Aucune notification à afficher.", "option": {} } }, @@ -2504,10 +2533,10 @@ }, "backgroundImageUrl": { "label": "URL de l'arrière-plan", - "placeholder": "", + "placeholder": "Commencez à taper pour rechercher des images locales", "group": { - "your": "", - "other": "" + "your": "Vos images", + "other": "Autres images" } }, "backgroundImageAttachment": { @@ -2570,7 +2599,7 @@ "label": "Couleur de l'icône" }, "clearColor": { - "label": "" + "label": "Enlever la couleur" }, "customCss": { "label": "CSS personnalisé pour ce tableau", @@ -3095,7 +3124,7 @@ "idle": "Inactif", "running": "En cours", "error": "Erreur", - "disabled": "" + "disabled": "Désactivé" }, "job": { "minecraftServerStatus": { @@ -3150,28 +3179,28 @@ "label": "Transcodage des médias" }, "networkController": { - "label": "" + "label": "Contrôleur réseau" }, "refreshNotifications": { - "label": "" + "label": "Mise à jour des notifications" }, "dockerContainers": { - "label": "" + "label": "Conteneurs Docker" } }, "interval": { - "seconds": "", - "minutes": "", - "hours": "", - "midnight": "", - "weeklyMonday": "" + "seconds": "Chaque {interval, plural, one {}=1 {seconde} other {# secondes}}", + "minutes": "Chaque {interval, plural, one {}=1 {minute} other {# minutes}}", + "hours": "Chaque {interval, plural, one {}=1 {heure} other {# heures}}", + "midnight": "Chaque jour à minuit", + "weeklyMonday": "Chaque semaine le lundi" }, "settings": { - "title": "" + "title": "Paramètres de la tâche pour {jobName}" }, "field": { "interval": { - "label": "" + "label": "Intervalle de planification" } } }, @@ -3237,7 +3266,7 @@ "updated": "Mis à jour {when}", "search": "Rechercher dans {count} conteneurs", "selected": "{selectCount} sur {totalCount} conteneurs sélectionnés", - "footer": "" + "footer": "Total des conteneurs {count}" }, "field": { "name": { @@ -3257,10 +3286,10 @@ }, "stats": { "cpu": { - "label": "" + "label": "Processeur" }, "memory": { - "label": "" + "label": "Mémoire" } }, "containerImage": { @@ -3271,7 +3300,7 @@ } }, "action": { - "title": "", + "title": "Actions", "start": { "label": "Début", "notification": { @@ -3376,7 +3405,7 @@ "title": "Ressources", "nodes": "Nœuds", "namespaces": "Espaces de noms", - "ingresses": "", + "ingresses": "Routes", "services": "Services", "pods": "Pods", "configmaps": "ConfigMaps", @@ -3445,7 +3474,7 @@ } }, "ingresses": { - "label": "", + "label": "Routes", "field": { "name": { "label": "Nom" @@ -3716,7 +3745,7 @@ "certificates": { "label": "Certificats", "hostnames": { - "label": "" + "label": "Noms d'hôtes" } } }, @@ -4117,25 +4146,25 @@ "certificate": { "field": { "hostname": { - "label": "" + "label": "Nom d'Hôte" }, "subject": { - "label": "" + "label": "Sujet" }, "issuer": { - "label": "" + "label": "Émetteur" }, "validFrom": { - "label": "" + "label": "Valable à partir du" }, "validTo": { - "label": "" + "label": "Valable jusqu'au" }, "serialNumber": { - "label": "" + "label": "Numéro de série" }, "fingerprint": { - "label": "" + "label": "Empreinte" } }, "page": { @@ -4146,19 +4175,19 @@ "title": "Il n'y a pas encore de certificats" }, "invalid": { - "title": "", - "description": "" + "title": "Certificat invalide", + "description": "Impossible d'analyser le certificat" }, "expires": "Expire le {when}", - "toHostnames": "" + "toHostnames": "Noms d'hôtes de confiance" }, "hostnames": { - "title": "", - "description": "", + "title": "Noms d'hôtes de certificat de confiance", + "description": "Certains certificats ne permettent pas au domaine spécifique que Homarr utilise pour les demander, à cause de cela, tous les noms d'hôtes de confiance avec leurs vignettes de certificat sont utilisés pour contourner ces restrictions.", "noResults": { - "title": "" + "title": "Il n'y a pas encore de noms d'hôtes" }, - "toCertificates": "" + "toCertificates": "Certificats" } }, "action": { @@ -4190,19 +4219,29 @@ } }, "removeHostname": { - "label": "", - "confirm": "", + "label": "Supprimer le nom d'hôte de confiance", + "confirm": "Êtes-vous sûr de vouloir supprimer ce nom d'hôte de confiance ? Cela peut empêcher certaines intégrations de fonctionner.", "notification": { "success": { - "title": "", - "message": "" + "title": "Nom d'hôte supprimé", + "message": "Le nom d'hôte a été supprimé avec succès" }, "error": { - "title": "", - "message": "" + "title": "Nom d'hôte non supprimé", + "message": "Le nom d'hôte n'a pas pu être supprimé" } } } } + }, + "log": { + "level": { + "option": { + "debug": "Débogage", + "info": "Information", + "warn": "Avertissement", + "error": "Erreur" + } + } } } diff --git a/packages/translation/src/lang/he.json b/packages/translation/src/lang/he.json index 400d71937..70d4075e7 100644 --- a/packages/translation/src/lang/he.json +++ b/packages/translation/src/lang/he.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "יחס גלובלי" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "רשימת בקשות מדיה", "description": "ראה רשימה של כל בקשות המדיה ממופע Overseerr או Jellyseerr שלך", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/hr.json b/packages/translation/src/lang/hr.json index 7df115be9..ae24b9001 100644 --- a/packages/translation/src/lang/hr.json +++ b/packages/translation/src/lang/hr.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "", "description": "Pregledajte popis svih zahtjeva za medijima s vaše instance Overseerr ili Jellyseerr", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/hu.json b/packages/translation/src/lang/hu.json index e38e3c238..f8e775e44 100644 --- a/packages/translation/src/lang/hu.json +++ b/packages/translation/src/lang/hu.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "", "description": "Az Overseerr vagy Jellyseerr példány összes médiakérelmének listájának megtekintése", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/it.json b/packages/translation/src/lang/it.json index c258ca072..bd00fa292 100644 --- a/packages/translation/src/lang/it.json +++ b/packages/translation/src/lang/it.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "", "description": "Visualizza un elenco di tutte le richieste media dalla tua istanza Overseerr o Jellyseerr", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/ja.json b/packages/translation/src/lang/ja.json index 7cf1c9a73..cb8816f8c 100644 --- a/packages/translation/src/lang/ja.json +++ b/packages/translation/src/lang/ja.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "グローバル比" }, + "mediaReleases": { + "name": "メディアリリース", + "description": "異なる連係機能から、新しく追加されたメディアまたは今後のリリースを表示する", + "option": { + "layout": { + "label": "レイアウト", + "option": { + "backdrop": { + "label": "背景" + }, + "poster": { + "label": "ポスター" + } + } + }, + "showDescriptionTooltip": { + "label": "説明ツールチップを表示" + }, + "showType": { + "label": "メディアタイプのバッジを表示" + }, + "showSource": { + "label": "情報元の連携機能を表示" + } + }, + "length": { + "duration": "{length} 分" + } + }, "mediaRequests-requestList": { "name": "メディアリクエストリスト", "description": "Overseerr または Jellyseerr からの全てのメディアリクエストのリストを見る", @@ -2309,7 +2338,7 @@ "openProjectPage": "プロジェクトページを開く", "openReleasePage": "リリースページを開く", "releaseDescription": "リリースの説明", - "projectDescription": "", + "projectDescription": "プロジェクトの説明", "created": "作成日", "error": { "label": "エラー", @@ -2570,7 +2599,7 @@ "label": "アイコンの色" }, "clearColor": { - "label": "" + "label": "色を消去" }, "customCss": { "label": "このボードのカスタム CSS", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "デバッグ", + "info": "情報", + "warn": "警告", + "error": "エラー" + } + } } } diff --git a/packages/translation/src/lang/ko.json b/packages/translation/src/lang/ko.json index 694dd90a1..21151e2fb 100644 --- a/packages/translation/src/lang/ko.json +++ b/packages/translation/src/lang/ko.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "", "description": "오버서 또는 젤리서 인스턴스의 모든 미디어 요청 목록 보기", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/lt.json b/packages/translation/src/lang/lt.json index 550e4bee5..21b20d08d 100644 --- a/packages/translation/src/lang/lt.json +++ b/packages/translation/src/lang/lt.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "", "description": "Peržiūrėkite visų medijų užklausų iš \"Overseerr\" arba \"Jellyseerr\" sąrašą", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/lv.json b/packages/translation/src/lang/lv.json index 08a40a84a..623aeae94 100644 --- a/packages/translation/src/lang/lv.json +++ b/packages/translation/src/lang/lv.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "", "description": "Skatiet sarakstu ar visiem multimediju pieprasījumiem no jūsu Overseerr vai Jellyseerr instances", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/nl.json b/packages/translation/src/lang/nl.json index 47bb84ca4..bb661f559 100644 --- a/packages/translation/src/lang/nl.json +++ b/packages/translation/src/lang/nl.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "Globale verhouding" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "Media-aanvragen lijst", "description": "Bekijk een lijst met alle media-aanvragen van je Overseerr of Jellyseerr instantie", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/no.json b/packages/translation/src/lang/no.json index 07ae22b00..bf3d3f5de 100644 --- a/packages/translation/src/lang/no.json +++ b/packages/translation/src/lang/no.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "Global ratio" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "Liste over mediaspillforespørsler", "description": "Se en liste over alle medieforespørsler fra din Overseerr eller Jellyseerr instans", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/pl.json b/packages/translation/src/lang/pl.json index a551aebc5..95aa85513 100644 --- a/packages/translation/src/lang/pl.json +++ b/packages/translation/src/lang/pl.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "Wskaźnik globalny" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "Lista żądań multimediów", "description": "Zobacz listę wszystkich zapytań o media z Twoich instancji Overseerr lub Jellyseerr", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/pt.json b/packages/translation/src/lang/pt.json index f9c7af0ca..c1fd1166e 100644 --- a/packages/translation/src/lang/pt.json +++ b/packages/translation/src/lang/pt.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "", "description": "Veja uma lista de todas as solicitações de mídia da sua instância do Overseerr ou Jellyseerr", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/ro.json b/packages/translation/src/lang/ro.json index 2b43982cf..856fc449c 100644 --- a/packages/translation/src/lang/ro.json +++ b/packages/translation/src/lang/ro.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "", "description": "Vezi o listă cu toate cererile media de la instanțele Overseerr sau Jellyseerr", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/ru.json b/packages/translation/src/lang/ru.json index e6496e32d..9eda10fb7 100644 --- a/packages/translation/src/lang/ru.json +++ b/packages/translation/src/lang/ru.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "Общий рейтинг" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "Запросы медиаконтента", "description": "Список всех запросов на добавление медиаконтента из вашего экземпляра Overseerr или Jellyseerr", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/sk.json b/packages/translation/src/lang/sk.json index 679585644..876a68011 100644 --- a/packages/translation/src/lang/sk.json +++ b/packages/translation/src/lang/sk.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "Globálny pomer" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "Zoznam mediálnych požiadaviek", "description": "Zobrazenie zoznamu všetkých mediálnych požiadaviek z Overseerr alebo Jellyseerr", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/sl.json b/packages/translation/src/lang/sl.json index 24e1d848c..647fd6af6 100644 --- a/packages/translation/src/lang/sl.json +++ b/packages/translation/src/lang/sl.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "", "description": "Oglejte si seznam vseh zahtevkov za medije iz vašega primera Overseerr ali Jellyseerr.", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/sv.json b/packages/translation/src/lang/sv.json index e8bfa6db2..763d2bbe2 100644 --- a/packages/translation/src/lang/sv.json +++ b/packages/translation/src/lang/sv.json @@ -165,7 +165,7 @@ "action": { "login": { "label": "Logga in", - "labelWith": "", + "labelWith": "Logga in med {provider}", "notification": { "success": { "title": "Inloggning lyckades", @@ -251,10 +251,10 @@ "label": "Byt bild", "notification": { "success": { - "message": "" + "message": "Bilden har ändrats" }, "error": { - "message": "" + "message": "Bilden kunde inte ändras" }, "toLarge": { "title": "Bilden är för stor", @@ -267,10 +267,10 @@ "confirm": "Är du säker på att du vill ta bort bilden?", "notification": { "success": { - "message": "" + "message": "Bilden har tagits bort" }, "error": { - "message": "" + "message": "Kunde inte ta bort bilden" } } } @@ -278,10 +278,10 @@ "editProfile": { "notification": { "success": { - "message": "" + "message": "Profilen har uppdaterats" }, "error": { - "message": "" + "message": "Det gick inte att uppdatera profilen" } } }, @@ -291,11 +291,11 @@ "confirm": "Är du säker på att du vill ta bort användaren {username} och användarens inställningar?" }, "select": { - "label": "", - "notFound": "" + "label": "Välj användare", + "notFound": "Ingen användare hittad" }, "transfer": { - "label": "" + "label": "Välj ny ägare" } } }, @@ -321,7 +321,7 @@ "item": { "admin": { "label": "Administratör", - "description": "" + "description": "Medlemmar med denna behörighet har full tillgång till alla funktioner och inställningar" } } }, @@ -329,11 +329,11 @@ "title": "Applikationer", "item": { "create": { - "label": "", + "label": "Skapa applikationer", "description": "Tillåt medlemmar att addera appar" }, "use-all": { - "label": "", + "label": "Använd alla applikationer", "description": "Tillåt medlemmar att addera valfri applikation på sina tavlor" }, "modify-all": { @@ -418,8 +418,8 @@ "title": "Sökmotorer", "item": { "create": { - "label": "", - "description": "" + "label": "Skapa sökmotorer", + "description": "Tillåt medlemmar att skapa sökmotorer" }, "modify-all": { "label": "", @@ -468,7 +468,7 @@ "label": "Lägg till en medlem" }, "removeMember": { - "label": "", + "label": "Ta bort användare", "confirm": "Är du säker på att du vill ta bort användaren {user} från denna grupp?" }, "delete": { @@ -487,7 +487,7 @@ "changePermissions": { "notification": { "success": { - "title": "", + "title": "Behörigheter sparade", "message": "" }, "error": { @@ -507,18 +507,18 @@ } }, "select": { - "label": "", + "label": "Välj en grupp", "notFound": "" }, "settings": { "board": { "notification": { "success": { - "title": "", + "title": "Inställningarna sparades", "message": "" }, "error": { - "title": "", + "title": "Kunde inte spara inställningarna", "message": "" } } @@ -738,7 +738,7 @@ "description": { "expired": "", "notYetValid": "", - "untrusted": "", + "untrusted": "Certifikatet är inte betrott.", "hostnameMismatch": "" }, "alert": { @@ -773,7 +773,7 @@ }, "notification": { "success": { - "title": "", + "title": "Betrodda certifikat", "message": "" }, "error": { @@ -789,7 +789,7 @@ }, "notification": { "success": { - "title": "", + "title": "Betrodda certifikat", "message": "" }, "error": { @@ -902,7 +902,7 @@ }, "secrets": { "title": "", - "lastUpdated": "", + "lastUpdated": "Senast uppdaterad {date}", "notSet": { "label": "", "tooltip": "" @@ -1075,7 +1075,7 @@ "menu": { "switchToDarkMode": "Byt till mörkt läge", "switchToLightMode": "Byt till ljust läge", - "management": "", + "management": "Administration", "preferences": "Dina inställningar", "logout": "Logga ut", "login": "Logga in", @@ -2079,6 +2079,35 @@ }, "globalRatio": "" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "", "description": "Se en lista över alla medieförfrågningar från din installation av Overseerr- eller Jellyseerr", @@ -2603,7 +2632,7 @@ }, "isPublic": { "label": "Allmän", - "description": "" + "description": "Offentliga tavlor är tillgängliga för alla, även till de utan ett konto." } }, "content": { @@ -2748,12 +2777,12 @@ "management": { "metaTitle": "", "title": { - "morning": "", - "afternoon": "", - "evening": "" + "morning": "God morgon, {username}", + "afternoon": "God eftermiddag, {username}", + "evening": "God kväll, {username}" }, "notFound": { - "title": "", + "title": "Hittades ej", "text": "" }, "navbar": { @@ -2761,9 +2790,9 @@ "home": "Hem", "boards": "Tavlor", "apps": "Applikationer", - "integrations": "", + "integrations": "Integrationer", "searchEngies": "Sökmotorer", - "medias": "", + "medias": "Media", "users": { "label": "Användare", "items": { @@ -2775,12 +2804,12 @@ "tools": { "label": "Verktyg", "items": { - "docker": "", + "docker": "Docker", "kubernetes": "", - "logs": "", - "api": "", - "certificates": "", - "tasks": "" + "logs": "Loggning", + "api": "API", + "certificates": "Certifikat", + "tasks": "Uppgifter" } }, "settings": "Inställningar", @@ -2788,7 +2817,7 @@ "label": "Hjälp", "items": { "documentation": "Dokumentation", - "submitIssue": "", + "submitIssue": "Skicka in ett ärende", "discord": "Community Discord", "sourceCode": "Källkod" } @@ -2802,25 +2831,25 @@ "board": "Tavlor", "user": "Användare", "invite": "Inbjudningar", - "integration": "", + "integration": "Integrationer", "app": "Applikationer", "group": "Grupper" }, "statisticLabel": { "boards": "Tavlor", - "resources": "", - "authentication": "", - "authorization": "" + "resources": "Resurser", + "authentication": "Autentisering", + "authorization": "Auktorisering" } }, "board": { "title": "Dina tavlor", "action": { "new": { - "label": "" + "label": "Ny tavla" }, "open": { - "label": "" + "label": "Öppna tavla" }, "settings": { "label": "Inställningar" @@ -2835,12 +2864,12 @@ "setMobileHomeBoard": { "label": "Ange som din mobila starttavla", "badge": { - "label": "", + "label": "Mobil", "tooltip": "Den här tavlan kommer att användas som din mobila starttavla" } }, "duplicate": { - "label": "" + "label": "Duplicera tavlan" }, "delete": { "label": "Radera permanent", @@ -2851,8 +2880,8 @@ } }, "visibility": { - "public": "", - "private": "" + "public": "Den här tavlan är offentlig", + "private": "Den här tavlan är privat" }, "modal": { "createBoard": { @@ -2865,11 +2894,11 @@ } }, "media": { - "includeFromAllUsers": "" + "includeFromAllUsers": "Inkludera media från alla användare" }, "user": { - "back": "", - "fieldsDisabledExternalProvider": "", + "back": "Tillbaka till användare", + "fieldsDisabledExternalProvider": "Vissa fält är inaktiverade eftersom de hanteras av en extern autentiseringsleverantör.", "setting": { "general": { "title": "Allmänt", @@ -2878,8 +2907,8 @@ "board": { "title": "Starttavla", "type": { - "general": "", - "mobile": "" + "general": "Generellt", + "mobile": "Mobil" } }, "search": "Sökning", @@ -2899,14 +2928,14 @@ "title": "Användare" }, "edit": { - "metaTitle": "" + "metaTitle": "Redigera användare {username}" }, "create": { "metaTitle": "Addera användare", "title": "Addera ny användare", "step": { "personalInformation": { - "label": "" + "label": "Personlig information" }, "security": { "label": "Säkerhet" @@ -2914,13 +2943,13 @@ "groups": { "label": "Grupper", "title": "Välj alla grupper användare skall tillhöra", - "description": "" + "description": "Gruppen {everyoneGroup} adderas till alla användare och kan inte tas bort." }, "review": { - "label": "" + "label": "Granska" }, "completed": { - "title": "Användare har adderats" + "title": "Användaren har adderats" }, "error": { "title": "Misslyckades med att addera användaren" @@ -2939,10 +2968,10 @@ "description": "Efter giltighetsdatumet är en inbjudan inte längre giltig och mottagaren kan inte addera ett konto." }, "copy": { - "title": "", - "description": "", + "title": "Kopiera inbjudan", + "description": "Din inbjudan har adderats. Efter att detta fönster stängts kommer inte längre att kunna kopiera denna länk. Om du inte längre vill bjuda in denna person kan du ta bort denna inbjudan när som helst.", "link": "Länk till inbjudan", - "button": "" + "button": "Kopiera & Stäng" }, "delete": { "title": "Ta bort inbjudan", @@ -2951,13 +2980,13 @@ }, "field": { "id": { - "label": "" + "label": "ID" }, "creator": { "label": "Adderad av" }, "expirationDate": { - "label": "Giltighetsdatum" + "label": "Giltig tom" }, "token": { "label": "" @@ -2966,7 +2995,7 @@ } }, "group": { - "back": "", + "back": "Tillbaka till grupper", "setting": { "general": { "title": "Allmänt", @@ -2975,21 +3004,21 @@ "ownerOfGroupDeleted": "Ägaren av denna grupp har tagits bort. Den har för närvarande ingen ägare." }, "setting": { - "title": "", - "alert": "", + "title": "Inställningar", + "alert": "Gruppinställningar prioriteras av ordningen på grupperna i listan. De övre inställningarna prioriteras högre än de nedre inställningarna.", "board": { "title": "Tavlor" } }, "members": { "title": "Medlemmar", - "search": "", - "notFound": "" + "search": "Hitta en medlem", + "notFound": "Inga medlemmar hittades" }, "permissions": { "title": "Behörigheter", "form": { - "unsavedChanges": "" + "unsavedChanges": "Du har ändringar som inte sparats!" } } } @@ -2998,10 +3027,10 @@ "title": "Inställningar", "notification": { "success": { - "message": "" + "message": "Inställningarna har sparats" }, "error": { - "message": "" + "message": "Kunde inte spara inställningarna" } }, "section": { @@ -3163,8 +3192,8 @@ "seconds": "", "minutes": "", "hours": "", - "midnight": "", - "weeklyMonday": "" + "midnight": "Varje dag vid midnatt", + "weeklyMonday": "Varje vecka på måndag" }, "settings": { "title": "" @@ -3213,11 +3242,11 @@ }, "about": { "version": "", - "text": "", + "text": "Homarr är ett open source-projekt som underhålls av volontärer. Tack vare dessa människor har Homarr vuxit och utvecklats sedan 2021. Vårt team arbetar på distans från många olika länder med Homarr på sin fritid utan ersättning.", "accordion": { "contributors": { - "title": "", - "subtitle": "" + "title": "Medarbetare", + "subtitle": "{count} upprätthåller kod & Homarr" }, "translators": { "title": "Översättare", @@ -3234,7 +3263,7 @@ "docker": { "title": "", "table": { - "updated": "", + "updated": "Uppdaterad {when}", "search": "", "selected": "", "footer": "" @@ -3244,7 +3273,7 @@ "label": "Namn" }, "state": { - "label": "Läge", + "label": "Status", "option": { "created": "Adderad", "running": "Körs", @@ -3325,7 +3354,7 @@ } }, "refresh": { - "label": "", + "label": "Uppdatera", "notification": { "success": { "title": "", @@ -4140,7 +4169,7 @@ }, "page": { "list": { - "title": "", + "title": "Betrodda certifikat", "description": "", "noResults": { "title": "" @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/tr.json b/packages/translation/src/lang/tr.json index be9e3d41a..e289ee0eb 100644 --- a/packages/translation/src/lang/tr.json +++ b/packages/translation/src/lang/tr.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "Genel Oran" }, + "mediaReleases": { + "name": "Medya yayınları", + "description": "Farklı entegrasyonlardan yeni eklenen medyaları veya yakında çıkacak yayınları göster", + "option": { + "layout": { + "label": "Düzen", + "option": { + "backdrop": { + "label": "Arka plan" + }, + "poster": { + "label": "Afiş" + } + } + }, + "showDescriptionTooltip": { + "label": "Açıklama ipucunu göster" + }, + "showType": { + "label": "Medya türü rozetini göster" + }, + "showSource": { + "label": "Kaynak entegrasyonunu göster" + } + }, + "length": { + "duration": "{length} dk" + } + }, "mediaRequests-requestList": { "name": "Medya İstekleri Listesi", "description": "Overseerr veya Jellyseerr uygulamanızdan gelen tüm medya taleplerinin bir listesini görün", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "Hata ayıklama", + "info": "Bilgi", + "warn": "Uyarı", + "error": "Hata" + } + } } } diff --git a/packages/translation/src/lang/uk.json b/packages/translation/src/lang/uk.json index fc27a9ae7..351ae32e2 100644 --- a/packages/translation/src/lang/uk.json +++ b/packages/translation/src/lang/uk.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "Загальний рейтинг" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "Список медіа запитів", "description": "Перегляньте список усіх медіазапитів від ваших Overseerr або Jellyseerr", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/vi.json b/packages/translation/src/lang/vi.json index e1ac2320b..1e0cc29be 100644 --- a/packages/translation/src/lang/vi.json +++ b/packages/translation/src/lang/vi.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "", "description": "Xem danh sách các yêu cầu đa phương tiện từ Overseerr hoặc Jellyseerr của bạn", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/translation/src/lang/zh.json b/packages/translation/src/lang/zh.json index abfdc51f5..0593f074f 100644 --- a/packages/translation/src/lang/zh.json +++ b/packages/translation/src/lang/zh.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "全局速率" }, + "mediaReleases": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "backdrop": { + "label": "" + }, + "poster": { + "label": "" + } + } + }, + "showDescriptionTooltip": { + "label": "" + }, + "showType": { + "label": "" + }, + "showSource": { + "label": "" + } + }, + "length": { + "duration": "" + } + }, "mediaRequests-requestList": { "name": "多媒體請求列表", "description": "查看 Overrseerr 或 Jellyseerr 中所有多媒體請求列表", @@ -4204,5 +4233,15 @@ } } } + }, + "log": { + "level": { + "option": { + "debug": "", + "info": "", + "warn": "", + "error": "" + } + } } } diff --git a/packages/ui/package.json b/packages/ui/package.json index 8956abb05..23982b1b6 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -30,14 +30,14 @@ "@homarr/log": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", - "@mantine/core": "^8.2.1", - "@mantine/dates": "^8.2.1", - "@mantine/hooks": "^8.2.1", + "@mantine/core": "^8.2.2", + "@mantine/dates": "^8.2.2", + "@mantine/hooks": "^8.2.2", "@tabler/icons-react": "^3.34.1", "mantine-react-table": "2.0.0-beta.9", - "next": "15.4.4", - "react": "19.1.0", - "react-dom": "19.1.0", + "next": "15.4.5", + "react": "19.1.1", + "react-dom": "19.1.1", "svgson": "^5.3.1" }, "devDependencies": { @@ -45,7 +45,7 @@ "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "@types/css-modules": "^1.0.5", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/validation/package.json b/packages/validation/package.json index 6610ee5e6..2b3cfb717 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -31,7 +31,7 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/validation/src/user.ts b/packages/validation/src/user.ts index b8d78e8ee..16bc593a0 100644 --- a/packages/validation/src/user.ts +++ b/packages/validation/src/user.ts @@ -16,7 +16,7 @@ export const passwordRequirements = [ { check: regexCheck(/[a-z]/), value: "lowercase" }, { check: regexCheck(/[A-Z]/), value: "uppercase" }, { check: regexCheck(/\d/), value: "number" }, - { check: regexCheck(/[$&+,:;=?@#|'<>.^*()%!-]/), value: "special" }, + { check: regexCheck(/[$&+,:;=?@#|'<>.^*()%!\-~`"_/\\[\]{}]/), value: "special" }, ] satisfies { check: (value: string) => boolean; value: keyof TranslationObject["user"]["field"]["password"]["requirement"]; diff --git a/packages/widgets/package.json b/packages/widgets/package.json index 2ad9c69aa..47209132c 100644 --- a/packages/widgets/package.json +++ b/packages/widgets/package.json @@ -48,9 +48,9 @@ "@homarr/translation": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", - "@mantine/charts": "^8.2.1", - "@mantine/core": "^8.2.1", - "@mantine/hooks": "^8.2.1", + "@mantine/charts": "^8.2.2", + "@mantine/core": "^8.2.2", + "@mantine/hooks": "^8.2.2", "@tabler/icons-react": "^3.34.1", "@tiptap/extension-color": "2.26.1", "@tiptap/extension-highlight": "2.26.1", @@ -71,9 +71,9 @@ "clsx": "^2.1.1", "dayjs": "^1.11.13", "mantine-react-table": "2.0.0-beta.9", - "next": "15.4.4", - "react": "19.1.0", - "react-dom": "19.1.0", + "next": "15.4.5", + "react": "19.1.1", + "react-dom": "19.1.1", "react-markdown": "^10.1.0", "recharts": "^2.15.4", "video.js": "^8.23.3", @@ -84,7 +84,7 @@ "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "@types/video.js": "^7.3.58", - "eslint": "^9.31.0", - "typescript": "^5.8.3" + "eslint": "^9.32.0", + "typescript": "^5.9.2" } } diff --git a/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx b/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx index 556322084..289936b6d 100644 --- a/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx +++ b/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx @@ -19,18 +19,19 @@ import { Title, Tooltip, } from "@mantine/core"; -import type { CheckboxProps } from "@mantine/core"; import type { FormErrors } from "@mantine/form"; import { useDebouncedValue } from "@mantine/hooks"; import { IconAlertTriangleFilled, IconBrandDocker, + IconCopy, + IconCopyCheckFilled, IconEdit, + IconPackageImport, IconPlus, - IconSquare, - IconSquareCheck, IconTrash, IconTriangleFilled, + IconZoomScan, } from "@tabler/icons-react"; import { escapeForRegEx } from "@tiptap/react"; @@ -511,33 +512,37 @@ interface ReleasesRepositoryImport extends ReleasesRepository { interface ImportRepositorySelectProps { repository: ReleasesRepositoryImport; + checked: boolean; integration?: Integration; versionFilterPrecisionOptions: string[]; + disabled: boolean; onImageSelectionChanged?: (isSelected: boolean) => void; } const ImportRepositorySelect = ({ repository, + checked, integration, versionFilterPrecisionOptions, - onImageSelectionChanged, + disabled = false, + onImageSelectionChanged = undefined, }: ImportRepositorySelectProps) => { const tRepository = useScopedI18n("widget.releases.option.repositories"); - const checkBoxProps: CheckboxProps = !onImageSelectionChanged - ? { - disabled: true, - checked: true, - } - : { - onChange: (event) => onImageSelectionChanged(event.currentTarget.checked), - }; return ( - + { + if (onImageSelectionChanged) { + onImageSelectionChanged(!checked); + } + }} label={ - + {repository.identifier} } - {...checkBoxProps} /> {repository.versionFilter && ( @@ -566,7 +570,7 @@ const ImportRepositorySelect = ({ )} - + {integration ? ( (({ innerProps, () => docker.data?.containers.reduce((acc, container) => { const [maybeSource, maybeIdentifierAndVersion] = container.image.split(/\/(.*)/); - const hasSource = maybeSource && maybeSource in sourceToProviderKind; + const hasSource = maybeSource && maybeSource in containerImageToProviderKind; const source = hasSource ? maybeSource : "docker.io"; - const identifierAndVersion = hasSource ? maybeIdentifierAndVersion : container.image; + const [identifier, version] = + hasSource && maybeIdentifierAndVersion ? maybeIdentifierAndVersion.split(":") : container.image.split(":"); - if (!identifierAndVersion) return acc; + if (!identifier) return acc; - const providerKey = sourceToProviderKind[source]; + const providerKind = containerImageToProviderKind[source] ?? "dockerHub"; const integrationId = Object.values(innerProps.integrations).find( - (integration) => integration.kind === providerKey, + (integration) => integration.kind === providerKind, )?.id; - const [identifier, version] = identifierAndVersion.split(":"); - - if (!identifier || !integrationId) return acc; - - if ( - acc.some( - (item) => - item.providerIntegrationId !== undefined && - innerProps.integrations[item.providerIntegrationId]?.kind === providerKey && - item.identifier === identifier, - ) - ) + if (acc.some((item) => item.providerIntegrationId === integrationId && item.identifier === identifier)) return acc; acc.push({ @@ -647,10 +641,7 @@ const RepositoryImportModal = createModal(({ innerProps, name: formatIdentifierName(identifier), versionFilter: version ? parseImageVersionToVersionFilter(version) : undefined, alreadyImported: innerProps.repositories.some( - (item) => - item.providerIntegrationId !== undefined && - innerProps.integrations[item.providerIntegrationId]?.kind === providerKey && - item.identifier === identifier, + (item) => item.providerIntegrationId === integrationId && item.identifier === identifier, ), }); return acc; @@ -693,7 +684,7 @@ const RepositoryImportModal = createModal(({ innerProps, - }> + }> {tRepository("importRepositories.listFoundImages")} {allImagesImported && ( @@ -704,52 +695,85 @@ const RepositoryImportModal = createModal(({ innerProps, - {!allImagesImported && - importRepositories - .filter((repository) => !repository.alreadyImported) - .map((repository) => { - const integration = repository.providerIntegrationId - ? innerProps.integrations[repository.providerIntegrationId] - : undefined; + {!allImagesImported && ( + + + + + - return ( - - isSelected - ? setSelectedImages([...selectedImages, repository]) - : setSelectedImages(selectedImages.filter((img) => img !== repository)) - } - /> - ); - })} + + + {importRepositories + .filter((repository) => !repository.alreadyImported) + .map((repository) => { + const integration = repository.providerIntegrationId + ? innerProps.integrations[repository.providerIntegrationId] + : undefined; + + return ( + + isSelected + ? setSelectedImages([...selectedImages, repository]) + : setSelectedImages(selectedImages.filter((img) => img !== repository)) + } + /> + ); + })} + + )} - }> + }> {tRepository("importRepositories.listAlreadyImportedImages")} - {anyImagesImported && - importRepositories - .filter((repository) => repository.alreadyImported) - .map((repository) => { - const integration = repository.providerIntegrationId - ? innerProps.integrations[repository.providerIntegrationId] - : undefined; + {anyImagesImported && ( + + {importRepositories + .filter((repository) => repository.alreadyImported) + .map((repository) => { + const integration = repository.providerIntegrationId + ? innerProps.integrations[repository.providerIntegrationId] + : undefined; - return ( - - ); - })} + return ( + + ); + })} + + )} @@ -774,9 +798,11 @@ const RepositoryImportModal = createModal(({ innerProps, size: "xl", }); -const sourceToProviderKind: Record = { +const containerImageToProviderKind: Record = { "ghcr.io": "github", "docker.io": "dockerHub", + "lscr.io": "linuxServerIO", + "quay.io": "quay", }; const parseImageVersionToVersionFilter = (imageVersion: string): ReleasesVersionFilter | undefined => { diff --git a/packages/widgets/src/app/component.tsx b/packages/widgets/src/app/component.tsx index e6f3d70ee..a89532063 100644 --- a/packages/widgets/src/app/component.tsx +++ b/packages/widgets/src/app/component.tsx @@ -1,20 +1,25 @@ "use client"; import type { PropsWithChildren } from "react"; +import { Suspense } from "react"; import { Flex, Text, Tooltip, UnstyledButton } from "@mantine/core"; +import { IconLoader } from "@tabler/icons-react"; import combineClasses from "clsx"; import { clientApi } from "@homarr/api/client"; import { useRequiredBoard } from "@homarr/boards/context"; import { useSettings } from "@homarr/settings"; import { useRegisterSpotlightContextResults } from "@homarr/spotlight"; +import { useI18n } from "@homarr/translation/client"; import { MaskedOrNormalImage } from "@homarr/ui"; import type { WidgetComponentProps } from "../definition"; import classes from "./app.module.css"; +import { PingDot } from "./ping/ping-dot"; import { PingIndicator } from "./ping/ping-indicator"; export default function AppWidget({ options, isEditMode, height, width }: WidgetComponentProps<"app">) { + const t = useI18n(); const settings = useSettings(); const board = useRequiredBoard(); const [app] = clientApi.app.byId.useSuspenseQuery( @@ -92,7 +97,9 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget {options.pingEnabled && !settings.forceDisableStatus && !board.disableStatus && app.href ? ( - + }> + + ) : null} ); diff --git a/packages/widgets/src/app/ping/ping-indicator.tsx b/packages/widgets/src/app/ping/ping-indicator.tsx index a00ec2640..a74a6c79f 100644 --- a/packages/widgets/src/app/ping/ping-indicator.tsx +++ b/packages/widgets/src/app/ping/ping-indicator.tsx @@ -1,9 +1,8 @@ import { useState } from "react"; -import { IconCheck, IconLoader, IconX } from "@tabler/icons-react"; +import { IconCheck, IconX } from "@tabler/icons-react"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; -import { useI18n } from "@homarr/translation/client"; import { PingDot } from "./ping-dot"; @@ -12,8 +11,17 @@ interface PingIndicatorProps { } export const PingIndicator = ({ href }: PingIndicatorProps) => { - const t = useI18n(); - const [pingResult, setPingResult] = useState(null); + const [ping] = clientApi.widget.app.ping.useSuspenseQuery( + { + url: href, + }, + { + refetchOnMount: false, + refetchOnWindowFocus: false, + }, + ); + + const [pingResult, setPingResult] = useState(ping); clientApi.widget.app.updatedPing.useSubscription( { url: href }, @@ -24,10 +32,6 @@ export const PingIndicator = ({ href }: PingIndicatorProps) => { }, ); - if (!pingResult) { - return ; - } - const isError = "error" in pingResult || pingResult.statusCode >= 500; return ( diff --git a/packages/widgets/src/firewall/component.tsx b/packages/widgets/src/firewall/component.tsx new file mode 100644 index 000000000..111ab85fa --- /dev/null +++ b/packages/widgets/src/firewall/component.tsx @@ -0,0 +1,397 @@ +"use client"; + +import { useCallback, useState } from "react"; +import { Accordion, Box, Center, Flex, Group, RingProgress, ScrollArea, Text } from "@mantine/core"; +import { useLocalStorage } from "@mantine/hooks"; +import { IconArrowBarDown, IconArrowBarUp, IconBrain, IconCpu, IconTopologyBus } from "@tabler/icons-react"; + +import { clientApi } from "@homarr/api/client"; +import type { FirewallInterface, FirewallInterfacesSummary } from "@homarr/integrations"; +import { useI18n } from "@homarr/translation/client"; + +import type { WidgetComponentProps } from "../definition"; +import { FirewallMenu } from "./firewall-menu"; +import { FirewallVersion } from "./firewall-version"; + +export interface Firewall { + label: string; + value: string; +} + +export default function FirewallWidget({ integrationIds, width, itemId }: WidgetComponentProps<"firewall">) { + const [selectedFirewall, setSelectedFirewall] = useState(""); + + const handleSelect = useCallback((value: string | null) => { + if (value !== null) { + setSelectedFirewall(value); + } else { + setSelectedFirewall("default_value"); + } + }, []); + + const firewallsCpuData = useUpdatingCpuStatus(integrationIds); + const firewallsMemoryData = useUpdatingMemoryStatus(integrationIds); + const firewallsVersionData = useUpdatingVersionStatus(integrationIds); + const firewallsInterfacesData = useUpdatingInterfacesStatus(integrationIds); + + const initialSelectedFirewall = firewallsVersionData[0] ? firewallsVersionData[0].integration.id : "undefined"; + const isTiny = width < 256; + + const [accordionValue, setAccordionValue] = useLocalStorage({ + key: `homarr-${itemId}-firewall`, + defaultValue: "interfaces", + }); + + const dropdownItems = firewallsVersionData.map((firewall) => ({ + label: firewall.integration.name, + value: firewall.integration.id, + })); + + const t = useI18n(); + + return ( + + + + + + + {/* Render CPU and Memory data */} + {firewallsCpuData + .filter(({ integration }) => integration.id === (selectedFirewall || initialSelectedFirewall)) + .map(({ summary, integration }) => ( + + {`${summary.total.toFixed(2)}%`} + + + } + sections={[ + { + value: Number(summary.total.toFixed(1)), + color: summary.total > 50 ? (summary.total < 75 ? "yellow" : "red") : "green", + }, + ]} + /> + ))} + {firewallsMemoryData + .filter(({ integration }) => integration.id === (selectedFirewall || initialSelectedFirewall)) + .map(({ summary, integration }) => ( + + {`${summary.percent.toFixed(1)}%`} + + + } + sections={[ + { + value: Number(summary.percent.toFixed(1)), + color: summary.percent > 50 ? (summary.percent < 75 ? "yellow" : "red") : "green", + }, + ]} + /> + ))} + + {firewallsInterfacesData + .filter(({ integration }) => integration.id === (selectedFirewall || initialSelectedFirewall)) + .map(({ summary }) => ( + + + }> + {t("widget.firewall.widget.interfaces.title")} + + + + {Array.isArray(summary) && summary.every((item) => Array.isArray(item.data)) ? ( + calculateBandwidth(summary).data.map(({ name, receive, transmit }) => ( + + + + {name} + + + + + + {formatBitsPerSec(transmit, 2)} + + + + + + {formatBitsPerSec(receive, 2)} + + + + )) + ) : ( + No data available + )} + + + + + ))} + + ); +} + +export const useUpdatingCpuStatus = (integrationIds: string[]) => { + const utils = clientApi.useUtils(); + const [firewallsCpuData] = clientApi.widget.firewall.getFirewallCpuStatus.useSuspenseQuery( + { + integrationIds, + }, + { + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + retry: false, + }, + ); + + clientApi.widget.firewall.subscribeFirewallCpuStatus.useSubscription( + { + integrationIds, + }, + { + onData: (data) => { + utils.widget.firewall.getFirewallCpuStatus.setData( + { + integrationIds, + }, + (prevData) => { + if (!prevData) { + return undefined; + } + + return prevData.map((item) => + item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item, + ); + }, + ); + }, + }, + ); + + return firewallsCpuData; +}; + +export const useUpdatingMemoryStatus = (integrationIds: string[]) => { + const utils = clientApi.useUtils(); + const [firewallsMemoryData] = clientApi.widget.firewall.getFirewallMemoryStatus.useSuspenseQuery( + { + integrationIds, + }, + { + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + retry: false, + }, + ); + + clientApi.widget.firewall.subscribeFirewallMemoryStatus.useSubscription( + { + integrationIds, + }, + { + onData: (data) => { + utils.widget.firewall.getFirewallMemoryStatus.setData( + { + integrationIds, + }, + (prevData) => { + if (!prevData) { + return undefined; + } + + return prevData.map((item) => + item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item, + ); + }, + ); + }, + }, + ); + + return firewallsMemoryData; +}; + +export const useUpdatingVersionStatus = (integrationIds: string[]) => { + const utils = clientApi.useUtils(); + const [firewallsVersionData] = clientApi.widget.firewall.getFirewallVersionStatus.useSuspenseQuery( + { + integrationIds, + }, + { + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + retry: false, + }, + ); + + clientApi.widget.firewall.subscribeFirewallVersionStatus.useSubscription( + { + integrationIds, + }, + { + onData: (data) => { + utils.widget.firewall.getFirewallVersionStatus.setData( + { + integrationIds, + }, + (prevData) => { + if (!prevData) { + return undefined; + } + + return prevData.map((item) => + item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item, + ); + }, + ); + }, + }, + ); + return firewallsVersionData; +}; + +export const useUpdatingInterfacesStatus = (integrationIds: string[]) => { + const utils = clientApi.useUtils(); + const [firewallsInterfacesData] = clientApi.widget.firewall.getFirewallInterfacesStatus.useSuspenseQuery( + { + integrationIds, + }, + { + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + retry: false, + }, + ); + + clientApi.widget.firewall.subscribeFirewallInterfacesStatus.useSubscription( + { + integrationIds, + }, + { + onData: (data) => { + utils.widget.firewall.getFirewallInterfacesStatus.setData( + { + integrationIds, + }, + (prevData) => { + if (!prevData) { + return undefined; + } + return prevData.map((item) => + item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item, + ); + }, + ); + }, + }, + ); + + return firewallsInterfacesData; +}; + +export function formatBitsPerSec(bytes: number, decimals: number): string { + if (bytes === 0) return "0 b/s"; + + const kilobyte = 1024; + const sizes = ["b/s", "kb/s", "Mb/s", "Gb/s", "Tb/s", "Pb/s", "Eb/s", "Zb/s", "Yb/s"]; + + const i = Math.floor(Math.log(bytes) / Math.log(kilobyte)); + + return `${parseFloat((bytes / Math.pow(kilobyte, i)).toFixed(decimals))} ${sizes[i]}`; +} + +export function calculateBandwidth(data: FirewallInterfacesSummary[]): { data: FirewallInterface[] } { + const result = { + data: [] as FirewallInterface[], + timestamp: new Date().toISOString(), + }; + + if (data.length > 1) { + const firstData = data[0]; + const secondData = data[1]; + + if (firstData && secondData) { + const time1 = new Date(firstData.timestamp); + const time2 = new Date(secondData.timestamp); + const timeDiffInSeconds = (time1.getTime() - time2.getTime()) / 1000; + + firstData.data.forEach((iface) => { + const ifaceName = iface.name; + const recv1 = iface.receive; + const trans1 = iface.transmit; + + const iface2 = secondData.data.find((i) => i.name === ifaceName); + + if (iface2) { + const recv2 = iface2.receive; + const trans2 = iface2.transmit; + const recvDiff = recv1 - recv2; + const transDiff = trans1 - trans2; + + result.data.push({ + name: ifaceName, + receive: (8 * recvDiff) / timeDiffInSeconds, + transmit: (8 * transDiff) / timeDiffInSeconds, + }); + } + }); + } + } + + return result; +} diff --git a/packages/widgets/src/firewall/firewall-menu.tsx b/packages/widgets/src/firewall/firewall-menu.tsx new file mode 100644 index 000000000..e96662dfb --- /dev/null +++ b/packages/widgets/src/firewall/firewall-menu.tsx @@ -0,0 +1,27 @@ +import { Box, Select } from "@mantine/core"; + +import type { Firewall } from "./component"; + +interface FirewallMenuProps { + onChange: (value: string | null) => void; + dropdownItems: Firewall[]; + selectedFirewall: string; + isTiny: boolean; +} + +export const FirewallMenu = ({ onChange, isTiny, dropdownItems, selectedFirewall }: FirewallMenuProps) => ( + +