From 180aa341405c7104f82a09dfc6448577066c9f5d Mon Sep 17 00:00:00 2001 From: vicke4 Date: Wed, 2 Apr 2025 11:07:48 +0530 Subject: [PATCH 01/53] fix(backups): awaiting initcronjobs in an attempt to fix backups cron --- apps/dokploy/server/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/server/server.ts b/apps/dokploy/server/server.ts index 921724cad..d10298cd5 100644 --- a/apps/dokploy/server/server.ts +++ b/apps/dokploy/server/server.ts @@ -46,7 +46,7 @@ void app.prepare().then(async () => { await initializeNetwork(); createDefaultTraefikConfig(); createDefaultServerTraefikConfig(); - initCronJobs(); + await initCronJobs(); await migration(); await sendDokployRestartNotifications(); } From 9d0f5bc8cd08f3e5803fd1d07c0f0a9abffdc02c Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Wed, 2 Apr 2025 00:31:55 -0600 Subject: [PATCH 02/53] Update package.json --- apps/dokploy/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 72d38531c..c095d1be4 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.21.0", + "version": "v0.21.1", "private": true, "license": "Apache-2.0", "type": "module", From 6f52edd845ae366cdf6c2620e6024336455c0c6f Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Wed, 2 Apr 2025 06:53:32 -0600 Subject: [PATCH 03/53] fix(backups): ensure initCronJobs is awaited before migration to improve backup reliability --- apps/dokploy/server/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/server/server.ts b/apps/dokploy/server/server.ts index d10298cd5..8ec533ffa 100644 --- a/apps/dokploy/server/server.ts +++ b/apps/dokploy/server/server.ts @@ -46,8 +46,8 @@ void app.prepare().then(async () => { await initializeNetwork(); createDefaultTraefikConfig(); createDefaultServerTraefikConfig(); - await initCronJobs(); await migration(); + await initCronJobs(); await sendDokployRestartNotifications(); } From d632e83799ab59b05a2f1b6fd9823c3b88fe7c98 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Wed, 2 Apr 2025 06:53:41 -0600 Subject: [PATCH 04/53] Update dokploy version to v0.21.2 in package.json --- apps/dokploy/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index c095d1be4..028ad20d7 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.21.1", + "version": "v0.21.2", "private": true, "license": "Apache-2.0", "type": "module", From 0bdaa812633ef4516d119550ca14239804c3c320 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Wed, 2 Apr 2025 07:10:19 -0600 Subject: [PATCH 05/53] fix(backups): replace findAdmin with db query to fetch admin by role for cron job initialization --- packages/server/src/utils/backups/index.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/server/src/utils/backups/index.ts b/packages/server/src/utils/backups/index.ts index d412df90d..b83d8279b 100644 --- a/packages/server/src/utils/backups/index.ts +++ b/packages/server/src/utils/backups/index.ts @@ -2,7 +2,6 @@ import path from "node:path"; import { getAllServers } from "@dokploy/server/services/server"; import { scheduleJob } from "node-schedule"; import { db } from "../../db/index"; -import { findAdmin } from "../../services/admin"; import { cleanUpDockerBuilder, cleanUpSystemPrune, @@ -14,13 +13,24 @@ import { getS3Credentials, scheduleBackup } from "./utils"; import type { BackupSchedule } from "@dokploy/server/services/backup"; import { startLogCleanup } from "../access-log/handler"; +import { member } from "@dokploy/server/db/schema"; +import { eq } from "drizzle-orm"; export const initCronJobs = async () => { console.log("Setting up cron jobs...."); - const admin = await findAdmin(); + const admin = await db.query.member.findFirst({ + where: eq(member.role, "owner"), + with: { + user: true, + }, + }); - if (admin?.user.enableDockerCleanup) { + if (!admin) { + return; + } + + if (admin.user.enableDockerCleanup) { scheduleJob("docker-cleanup", "0 0 * * *", async () => { console.log( `Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`, From 94c947e288d098b0d54aa5b015d62de96a0699b4 Mon Sep 17 00:00:00 2001 From: vicke4 Date: Thu, 3 Apr 2025 11:41:21 +0530 Subject: [PATCH 06/53] fix(backups): web-server backups auto-deletion --- packages/server/src/utils/backups/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/utils/backups/index.ts b/packages/server/src/utils/backups/index.ts index b83d8279b..e7510def7 100644 --- a/packages/server/src/utils/backups/index.ts +++ b/packages/server/src/utils/backups/index.ts @@ -106,8 +106,8 @@ export const keepLatestNBackups = async ( backup.prefix, ); - // --include "*.sql.gz" ensures nothing else other than the db backup files are touched by rclone - const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*.sql.gz" ${backupFilesPath}`; + // --include "*.sql.gz" or "*.zip" ensures nothing else other than the dokploy backup files are touched by rclone + const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? '.zip' : '.sql.gz'}" ${backupFilesPath}`; // when we pipe the above command with this one, we only get the list of files we want to delete const sortAndPickUnwantedBackups = `sort -r | tail -n +$((${backup.keepLatestCount}+1)) | xargs -I{}`; // this command deletes the files From e176def5b67ca74846fccb364a11b7bf2d1b9012 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 3 Apr 2025 06:12:08 +0000 Subject: [PATCH 07/53] [autofix.ci] apply automated fixes --- packages/server/src/utils/backups/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/utils/backups/index.ts b/packages/server/src/utils/backups/index.ts index e7510def7..6c9404064 100644 --- a/packages/server/src/utils/backups/index.ts +++ b/packages/server/src/utils/backups/index.ts @@ -107,7 +107,7 @@ export const keepLatestNBackups = async ( ); // --include "*.sql.gz" or "*.zip" ensures nothing else other than the dokploy backup files are touched by rclone - const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? '.zip' : '.sql.gz'}" ${backupFilesPath}`; + const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? ".zip" : ".sql.gz"}" ${backupFilesPath}`; // when we pipe the above command with this one, we only get the list of files we want to delete const sortAndPickUnwantedBackups = `sort -r | tail -n +$((${backup.keepLatestCount}+1)) | xargs -I{}`; // this command deletes the files From b9de05015ff14a1f631a37225cadf15eaffd34db Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 3 Apr 2025 00:17:09 -0600 Subject: [PATCH 08/53] fix(templates): add optional chaining to prevent errors when accessing template properties --- .../dashboard/project/add-template.tsx | 42 ++++++++++--------- packages/server/src/templates/processors.ts | 23 ++++++++-- 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/apps/dokploy/components/dashboard/project/add-template.tsx b/apps/dokploy/components/dashboard/project/add-template.tsx index 5dbbcd1da..8e9de54d9 100644 --- a/apps/dokploy/components/dashboard/project/add-template.tsx +++ b/apps/dokploy/components/dashboard/project/add-template.tsx @@ -307,7 +307,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => { > {templates?.map((template) => (
{ )} > - {template.version} + {template?.version}
{ )} > {template.name}
- {template.name} + {template?.name} {viewMode === "detailed" && - template.tags.length > 0 && ( + template?.tags?.length > 0 && (
- {template.tags.map((tag) => ( + {template?.tags?.map((tag) => ( { {viewMode === "detailed" && (
- {template.description} + {template?.description}
)} @@ -372,25 +372,27 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => { > {viewMode === "detailed" && (
- - - - {template.links.website && ( + {template?.links?.github && ( + + + )} + {template?.links?.website && ( + )} - {template.links.docs && ( + {template?.links?.docs && ( @@ -419,7 +421,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => { This will create an application from the{" "} - {template.name} template and add it to your + {template?.name} template and add it to your project. diff --git a/packages/server/src/templates/processors.ts b/packages/server/src/templates/processors.ts index 86d3cdf74..7ef774f8a 100644 --- a/packages/server/src/templates/processors.ts +++ b/packages/server/src/templates/processors.ts @@ -70,7 +70,7 @@ function processValue( schema: Schema, ): string { // First replace utility functions - let processedValue = value.replace(/\${([^}]+)}/g, (match, varName) => { + let processedValue = value?.replace(/\${([^}]+)}/g, (match, varName) => { // Handle utility functions if (varName === "domain") { return generateRandomDomain(schema); @@ -177,7 +177,14 @@ export function processDomains( variables: Record, schema: Schema, ): Template["domains"] { - if (!template?.config?.domains) return []; + if ( + !template?.config?.domains || + template.config.domains.length === 0 || + template.config.domains.every((domain) => !domain.serviceName) + ) { + return []; + } + return template?.config?.domains?.map((domain: DomainConfig) => ({ ...domain, host: domain.host @@ -194,7 +201,9 @@ export function processEnvVars( variables: Record, schema: Schema, ): Template["envs"] { - if (!template?.config?.env) return []; + if (!template?.config?.env || Object.keys(template.config.env).length === 0) { + return []; + } // Handle array of env vars if (Array.isArray(template.config.env)) { @@ -233,7 +242,13 @@ export function processMounts( variables: Record, schema: Schema, ): Template["mounts"] { - if (!template?.config?.mounts) return []; + if ( + !template?.config?.mounts || + template.config.mounts.length === 0 || + template.config.mounts.every((mount) => !mount.filePath && !mount.content) + ) { + return []; + } return template?.config?.mounts?.map((mount: MountConfig) => ({ filePath: processValue(mount.filePath, variables, schema), From 9a839de022fac468287934c35e2ded3bbda7defb Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 3 Apr 2025 00:22:29 -0600 Subject: [PATCH 09/53] feat(templates): add username and email generation using faker --- packages/server/src/templates/processors.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/server/src/templates/processors.ts b/packages/server/src/templates/processors.ts index 7ef774f8a..31e7861ad 100644 --- a/packages/server/src/templates/processors.ts +++ b/packages/server/src/templates/processors.ts @@ -1,3 +1,4 @@ +import { faker } from "@faker-js/faker"; import type { Schema } from "./index"; import { generateBase64, @@ -117,6 +118,14 @@ function processValue( return generateJwt(length); } + if (varName === "username") { + return faker.internet.userName().toLowerCase(); + } + + if (varName === "email") { + return faker.internet.email().toLowerCase(); + } + // If not a utility function, try to get from variables return variables[varName] || match; }); From 031d0ce315989cd4a12b6c9ab016871c8ac942c3 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 3 Apr 2025 00:22:57 -0600 Subject: [PATCH 10/53] Update dokploy version to v0.21.3 in package.json --- apps/dokploy/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 028ad20d7..34ca86faf 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.21.2", + "version": "v0.21.3", "private": true, "license": "Apache-2.0", "type": "module", From 8479f20205a5cca94d7cd9e021ea04aab713fd8f Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Fri, 4 Apr 2025 01:27:53 -0600 Subject: [PATCH 11/53] feat(handle-project): enhance project name validation to disallow starting with a number --- .../dashboard/projects/handle-project.tsx | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/apps/dokploy/components/dashboard/projects/handle-project.tsx b/apps/dokploy/components/dashboard/projects/handle-project.tsx index 85b8aea9a..dcb812419 100644 --- a/apps/dokploy/components/dashboard/projects/handle-project.tsx +++ b/apps/dokploy/components/dashboard/projects/handle-project.tsx @@ -31,9 +31,14 @@ import { toast } from "sonner"; import { z } from "zod"; const AddProjectSchema = z.object({ - name: z.string().min(1, { - message: "Name is required", - }), + name: z + .string() + .min(1, { + message: "Name is required", + }) + .regex(/^[a-zA-Z]/, { + message: "Project name cannot start with a number", + }), description: z.string().optional(), }); @@ -97,18 +102,6 @@ export const HandleProject = ({ projectId }: Props) => { ); }); }; - // useEffect(() => { - // const getUsers = async () => { - // const users = await authClient.admin.listUsers({ - // query: { - // limit: 100, - // }, - // }); - // console.log(users); - // }; - - // getUsers(); - // }); return ( From 36172491a41dc85d2c9372a6938b25fe7a4defc7 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Fri, 4 Apr 2025 01:55:29 -0600 Subject: [PATCH 12/53] refactor(websocket): streamline WebSocket server setup and client instantiation - Removed the request validation logic from the WebSocket connection handler. - Added a cleanup function to close the WebSocket server. - Introduced a singleton pattern for the WebSocket client to manage connections more efficiently. --- apps/dokploy/server/wss/drawer-logs.ts | 16 ++++++--------- apps/dokploy/utils/api.ts | 27 +++++++++++++++++++------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/apps/dokploy/server/wss/drawer-logs.ts b/apps/dokploy/server/wss/drawer-logs.ts index 404dfeee5..dcdeaad7d 100644 --- a/apps/dokploy/server/wss/drawer-logs.ts +++ b/apps/dokploy/server/wss/drawer-logs.ts @@ -1,5 +1,4 @@ import type http from "node:http"; -import { validateRequest } from "@dokploy/server/index"; import { applyWSSHandler } from "@trpc/server/adapters/ws"; import { WebSocketServer } from "ws"; import { appRouter } from "../api/root"; @@ -13,11 +12,13 @@ export const setupDrawerLogsWebSocketServer = ( path: "/drawer-logs", }); + // Set up tRPC WebSocket handler applyWSSHandler({ wss: wssTerm, router: appRouter, createContext: createTRPCContext as any, }); + server.on("upgrade", (req, socket, head) => { const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); @@ -31,13 +32,8 @@ export const setupDrawerLogsWebSocketServer = ( } }); - wssTerm.on("connection", async (ws, req) => { - const _url = new URL(req.url || "", `http://${req.headers.host}`); - const { user, session } = await validateRequest(req); - - if (!user || !session) { - ws.close(); - return; - } - }); + // Return cleanup function + return () => { + wssTerm.close(); + }; }; diff --git a/apps/dokploy/utils/api.ts b/apps/dokploy/utils/api.ts index 56197528b..7c003f481 100644 --- a/apps/dokploy/utils/api.ts +++ b/apps/dokploy/utils/api.ts @@ -27,15 +27,28 @@ const getWsUrl = () => { const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const host = window.location.host; + // Use the base URL for all tRPC WebSocket connections return `${protocol}${host}/drawer-logs`; }; -const wsClient = - typeof window !== "undefined" - ? createWSClient({ - url: getWsUrl() || "", - }) - : null; +// Singleton WebSocket client instance +let wsClientInstance: ReturnType | null = null; + +const getWsClient = () => { + if (typeof window === "undefined") return null; + + if (!wsClientInstance) { + wsClientInstance = createWSClient({ + url: getWsUrl() || "", + onClose: () => { + // Reset the instance when connection closes so it can be recreated + wsClientInstance = null; + }, + }); + } + + return wsClientInstance; +}; /** A set of type-safe react-query hooks for your tRPC API. */ export const api = createTRPCNext({ @@ -57,7 +70,7 @@ export const api = createTRPCNext({ splitLink({ condition: (op) => op.type === "subscription", true: wsLink({ - client: wsClient!, + client: getWsClient()!, }), false: splitLink({ condition: (op) => op.input instanceof FormData, From eff2657e70fc76429e15a67e3f433d3c24d27442 Mon Sep 17 00:00:00 2001 From: krokodaws <46326151+krokodaws@users.noreply.github.com> Date: Fri, 4 Apr 2025 19:21:30 +0300 Subject: [PATCH 13/53] fix: resolve incorrect endpoints for database bulk actions (#1626) Update bulk action endpoints for database services: - Use `/api/trpc/redis.start` and `/api/trpc/redis.stop` for Redis - Use `/api/trpc/postgres.start` and `/api/trpc/postgres.stop` for PostgreSQL - Retain `/api/trpc/compose.start` and `/api/trpc/compose.stop` for Docker Compose services Tested with a project including Gitea, Redis, and PostgreSQL. Bulk start/stop operations now function correctly for all service types. Closes #1626 --- .../pages/dashboard/project/[projectId].tsx | 66 ++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/pages/dashboard/project/[projectId].tsx b/apps/dokploy/pages/dashboard/project/[projectId].tsx index e3cfce16d..6c4ac4bcc 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId].tsx @@ -314,31 +314,43 @@ const Project = ( }; const applicationActions = { + start: api.application.start.useMutation(), + stop: api.application.stop.useMutation(), move: api.application.move.useMutation(), delete: api.application.delete.useMutation(), }; const postgresActions = { + start: api.postgres.start.useMutation(), + stop: api.postgres.stop.useMutation(), move: api.postgres.move.useMutation(), delete: api.postgres.remove.useMutation(), }; const mysqlActions = { + start: api.mysql.start.useMutation(), + stop: api.mysql.stop.useMutation(), move: api.mysql.move.useMutation(), delete: api.mysql.remove.useMutation(), }; const mariadbActions = { + start: api.mariadb.start.useMutation(), + stop: api.mariadb.stop.useMutation(), move: api.mariadb.move.useMutation(), delete: api.mariadb.remove.useMutation(), }; const redisActions = { + start: api.redis.start.useMutation(), + stop: api.redis.stop.useMutation(), move: api.redis.move.useMutation(), delete: api.redis.remove.useMutation(), }; const mongoActions = { + start: api.mongo.start.useMutation(), + stop: api.mongo.stop.useMutation(), move: api.mongo.move.useMutation(), delete: api.mongo.remove.useMutation(), }; @@ -348,7 +360,32 @@ const Project = ( setIsBulkActionLoading(true); for (const serviceId of selectedServices) { try { - await composeActions.start.mutateAsync({ composeId: serviceId }); + const service = filteredServices.find((s) => s.id === serviceId); + if (!service) continue; + + switch (service.type) { + case "application": + await applicationActions.start.mutateAsync({ applicationId: serviceId }); + break; + case "compose": + await composeActions.start.mutateAsync({ composeId: serviceId }); + break; + case "postgres": + await postgresActions.start.mutateAsync({ postgresId: serviceId }); + break; + case "mysql": + await mysqlActions.start.mutateAsync({ mysqlId: serviceId }); + break; + case "mariadb": + await mariadbActions.start.mutateAsync({ mariadbId: serviceId }); + break; + case "redis": + await redisActions.start.mutateAsync({ redisId: serviceId }); + break; + case "mongo": + await mongoActions.start.mutateAsync({ mongoId: serviceId }); + break; + } success++; } catch (_error) { toast.error(`Error starting service ${serviceId}`); @@ -368,7 +405,32 @@ const Project = ( setIsBulkActionLoading(true); for (const serviceId of selectedServices) { try { - await composeActions.stop.mutateAsync({ composeId: serviceId }); + const service = filteredServices.find((s) => s.id === serviceId); + if (!service) continue; + + switch (service.type) { + case "application": + await applicationActions.stop.mutateAsync({ applicationId: serviceId }); + break; + case "compose": + await composeActions.stop.mutateAsync({ composeId: serviceId }); + break; + case "postgres": + await postgresActions.stop.mutateAsync({ postgresId: serviceId }); + break; + case "mysql": + await mysqlActions.stop.mutateAsync({ mysqlId: serviceId }); + break; + case "mariadb": + await mariadbActions.stop.mutateAsync({ mariadbId: serviceId }); + break; + case "redis": + await redisActions.stop.mutateAsync({ redisId: serviceId }); + break; + case "mongo": + await mongoActions.stop.mutateAsync({ mongoId: serviceId }); + break; + } success++; } catch (_error) { toast.error(`Error stopping service ${serviceId}`); From 2c09b63bf9a25c38b0206acfb8d761dfa6f08eea Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Fri, 4 Apr 2025 19:19:09 +0200 Subject: [PATCH 14/53] feat: improve projects show grid --- apps/dokploy/components/dashboard/projects/show.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index 31ba80c8f..03ebe7a85 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -115,7 +115,7 @@ export const ShowProjects = () => {
)} -
+
{filteredProjects?.map((project) => { const emptyServices = project?.mariadb.length === 0 && From 5863e45c13916845c1b7581faae44c6c1a0b2a1c Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Fri, 4 Apr 2025 20:18:56 +0200 Subject: [PATCH 15/53] remove sensitive files on static build --- packages/server/src/utils/builders/static.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/server/src/utils/builders/static.ts b/packages/server/src/utils/builders/static.ts index c46bdf2e8..f7fc87cac 100644 --- a/packages/server/src/utils/builders/static.ts +++ b/packages/server/src/utils/builders/static.ts @@ -25,6 +25,12 @@ export const buildStatic = async ( ].join("\n"), ); + createFile( + buildAppDirectory, + ".dockerignore", + [".git", ".env", "Dockerfile", ".dockerignore"].join("\n"), + ); + await buildCustomDocker( { ...application, From d9c34c4524c37c1b8f6e5436ca8db9ae95f704ff Mon Sep 17 00:00:00 2001 From: Hoofei Date: Sun, 6 Apr 2025 12:09:30 +0800 Subject: [PATCH 16/53] Update Simplified Chinese --- .../public/locales/zh-Hans/common.json | 94 ++++++++++++++++++- .../public/locales/zh-Hans/settings.json | 40 ++++---- 2 files changed, 113 insertions(+), 21 deletions(-) diff --git a/apps/dokploy/public/locales/zh-Hans/common.json b/apps/dokploy/public/locales/zh-Hans/common.json index 0967ef424..67797618a 100644 --- a/apps/dokploy/public/locales/zh-Hans/common.json +++ b/apps/dokploy/public/locales/zh-Hans/common.json @@ -1 +1,93 @@ -{} +{ + "dashboard.title": "仪表盘", + "dashboard.overview": "概览", + "dashboard.projects": "项目", + "dashboard.servers": "服务器", + "dashboard.docker": "Docker", + "dashboard.monitoring": "监控", + "dashboard.settings": "设置", + "dashboard.logout": "退出登录", + "dashboard.profile": "个人资料", + "dashboard.terminal": "终端", + "dashboard.containers": "容器", + "dashboard.images": "镜像", + "dashboard.volumes": "卷", + "dashboard.networks": "网络", + + "button.create": "创建", + "button.edit": "编辑", + "button.delete": "删除", + "button.cancel": "取消", + "button.save": "保存", + "button.confirm": "确认", + "button.back": "返回", + "button.next": "下一步", + "button.finish": "完成", + + "status.running": "运行中", + "status.stopped": "已停止", + "status.error": "错误", + "status.pending": "等待中", + "status.success": "成功", + "status.failed": "失败", + + "form.required": "必填", + "form.invalid": "无效", + "form.submit": "提交", + "form.reset": "重置", + + "notification.success": "操作成功", + "notification.error": "操作失败", + "notification.warning": "警告", + "notification.info": "信息", + + "time.now": "刚刚", + "time.minutes": "分钟前", + "time.hours": "小时前", + "time.days": "天前", + + "filter.all": "全部", + "filter.active": "活跃", + "filter.inactive": "不活跃", + + "sort.asc": "升序", + "sort.desc": "降序", + + "search.placeholder": "搜索...", + "search.noResults": "无结果", + + "pagination.prev": "上一页", + "pagination.next": "下一页", + "pagination.of": "共 {0} 页", + + "error.notFound": "未找到", + "error.serverError": "服务器错误", + "error.unauthorized": "未授权", + "error.forbidden": "禁止访问", + + "loading": "加载中...", + "empty": "暂无数据", + "more": "更多", + "less": "收起", + + "project.create": "创建项目", + "project.edit": "编辑项目", + "project.delete": "删除项目", + "project.name": "项目名称", + "project.description": "项目描述", + + "service.create": "创建服务", + "service.edit": "编辑服务", + "service.delete": "删除服务", + "service.name": "服务名称", + "service.type": "服务类型", + + "domain.add": "添加域名", + "domain.remove": "移除域名", + + "environment.variables": "环境变量", + "environment.add": "添加环境变量", + "environment.edit": "编辑环境变量", + "environment.name": "变量名", + "environment.value": "变量值" +} diff --git a/apps/dokploy/public/locales/zh-Hans/settings.json b/apps/dokploy/public/locales/zh-Hans/settings.json index c74fb21f8..f7e8a38a8 100644 --- a/apps/dokploy/public/locales/zh-Hans/settings.json +++ b/apps/dokploy/public/locales/zh-Hans/settings.json @@ -1,17 +1,17 @@ { "settings.common.save": "保存", - "settings.common.enterTerminal": "进入终端", - "settings.server.domain.title": "域名设置", - "settings.server.domain.description": "添加域名到服务器", + "settings.common.enterTerminal": "终端", + "settings.server.domain.title": "服务器域名", + "settings.server.domain.description": "为您的服务器应用添加域名。", "settings.server.domain.form.domain": "域名", "settings.server.domain.form.letsEncryptEmail": "Let's Encrypt 邮箱", - "settings.server.domain.form.certificate.label": "证书", - "settings.server.domain.form.certificate.placeholder": "选择一个证书", + "settings.server.domain.form.certificate.label": "证书提供商", + "settings.server.domain.form.certificate.placeholder": "选择证书", "settings.server.domain.form.certificateOptions.none": "无", "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt", - "settings.server.webServer.title": "服务器设置", - "settings.server.webServer.description": "管理服务器", + "settings.server.webServer.title": "Web 服务器", + "settings.server.webServer.description": "重载或清理 Web 服务器。", "settings.server.webServer.actions": "操作", "settings.server.webServer.reload": "重新加载", "settings.server.webServer.watchLogs": "查看日志", @@ -19,40 +19,40 @@ "settings.server.webServer.server.label": "服务器", "settings.server.webServer.traefik.label": "Traefik", "settings.server.webServer.traefik.modifyEnv": "修改环境变量", - "settings.server.webServer.traefik.managePorts": "端口转发", - "settings.server.webServer.traefik.managePortsDescription": "添加或删除 Traefik 的其他端口", + "settings.server.webServer.traefik.managePorts": "额外端口映射", + "settings.server.webServer.traefik.managePortsDescription": "为 Traefik 添加或删除额外端口", "settings.server.webServer.traefik.targetPort": "目标端口", - "settings.server.webServer.traefik.publishedPort": "对外端口", + "settings.server.webServer.traefik.publishedPort": "发布端口", "settings.server.webServer.traefik.addPort": "添加端口", "settings.server.webServer.traefik.portsUpdated": "端口更新成功", "settings.server.webServer.traefik.portsUpdateError": "端口更新失败", - "settings.server.webServer.traefik.publishMode": "端口映射", + "settings.server.webServer.traefik.publishMode": "发布模式", "settings.server.webServer.storage.label": "存储空间", "settings.server.webServer.storage.cleanUnusedImages": "清理未使用的镜像", "settings.server.webServer.storage.cleanUnusedVolumes": "清理未使用的卷", "settings.server.webServer.storage.cleanStoppedContainers": "清理已停止的容器", - "settings.server.webServer.storage.cleanDockerBuilder": "清理 Docker Builder 与 系统缓存", + "settings.server.webServer.storage.cleanDockerBuilder": "清理 Docker Builder 和系统", "settings.server.webServer.storage.cleanMonitoring": "清理监控数据", "settings.server.webServer.storage.cleanAll": "清理所有内容", "settings.profile.title": "账户", - "settings.profile.description": "更改您的个人资料", + "settings.profile.description": "在此更改您的个人资料详情。", "settings.profile.email": "邮箱", "settings.profile.password": "密码", "settings.profile.avatar": "头像", "settings.appearance.title": "外观", - "settings.appearance.description": "自定义面板主题", + "settings.appearance.description": "自定义您的仪表盘主题。", "settings.appearance.theme": "主题", - "settings.appearance.themeDescription": "选择面板主题", + "settings.appearance.themeDescription": "为您的仪表盘选择主题", "settings.appearance.themes.light": "明亮", - "settings.appearance.themes.dark": "黑暗", - "settings.appearance.themes.system": "系统主题", + "settings.appearance.themes.dark": "暗黑", + "settings.appearance.themes.system": "跟随系统", "settings.appearance.language": "语言", - "settings.appearance.languageDescription": "选择面板语言", + "settings.appearance.languageDescription": "为您的仪表盘选择语言", - "settings.terminal.connectionSettings": "终端设置", - "settings.terminal.ipAddress": "IP", + "settings.terminal.connectionSettings": "连接设置", + "settings.terminal.ipAddress": "IP 地址", "settings.terminal.port": "端口", "settings.terminal.username": "用户名" } From 7ac74813435ddcdf8e8bcc43cae7a921d4897489 Mon Sep 17 00:00:00 2001 From: Hoofei Date: Sun, 6 Apr 2025 12:34:47 +0800 Subject: [PATCH 17/53] Update Simplified Chinese --- .../public/locales/zh-Hans/common.json | 17 +- .../public/locales/zh-Hans/settings.json | 123 ++++---- extract.js | 270 ++++++++++++++++++ merge-translations.js | 252 ++++++++++++++++ 4 files changed, 589 insertions(+), 73 deletions(-) create mode 100644 extract.js create mode 100644 merge-translations.js diff --git a/apps/dokploy/public/locales/zh-Hans/common.json b/apps/dokploy/public/locales/zh-Hans/common.json index 67797618a..d8faad7d4 100644 --- a/apps/dokploy/public/locales/zh-Hans/common.json +++ b/apps/dokploy/public/locales/zh-Hans/common.json @@ -13,7 +13,6 @@ "dashboard.images": "镜像", "dashboard.volumes": "卷", "dashboard.networks": "网络", - "button.create": "创建", "button.edit": "编辑", "button.delete": "删除", @@ -23,71 +22,57 @@ "button.back": "返回", "button.next": "下一步", "button.finish": "完成", - "status.running": "运行中", "status.stopped": "已停止", "status.error": "错误", "status.pending": "等待中", "status.success": "成功", "status.failed": "失败", - "form.required": "必填", "form.invalid": "无效", "form.submit": "提交", "form.reset": "重置", - "notification.success": "操作成功", "notification.error": "操作失败", "notification.warning": "警告", "notification.info": "信息", - "time.now": "刚刚", "time.minutes": "分钟前", "time.hours": "小时前", "time.days": "天前", - "filter.all": "全部", "filter.active": "活跃", "filter.inactive": "不活跃", - "sort.asc": "升序", "sort.desc": "降序", - "search.placeholder": "搜索...", "search.noResults": "无结果", - "pagination.prev": "上一页", "pagination.next": "下一页", "pagination.of": "共 {0} 页", - "error.notFound": "未找到", "error.serverError": "服务器错误", "error.unauthorized": "未授权", "error.forbidden": "禁止访问", - "loading": "加载中...", "empty": "暂无数据", "more": "更多", "less": "收起", - "project.create": "创建项目", "project.edit": "编辑项目", "project.delete": "删除项目", "project.name": "项目名称", "project.description": "项目描述", - "service.create": "创建服务", "service.edit": "编辑服务", "service.delete": "删除服务", "service.name": "服务名称", "service.type": "服务类型", - "domain.add": "添加域名", "domain.remove": "移除域名", - "environment.variables": "环境变量", "environment.add": "添加环境变量", "environment.edit": "编辑环境变量", "environment.name": "变量名", "environment.value": "变量值" -} +} \ No newline at end of file diff --git a/apps/dokploy/public/locales/zh-Hans/settings.json b/apps/dokploy/public/locales/zh-Hans/settings.json index f7e8a38a8..060332384 100644 --- a/apps/dokploy/public/locales/zh-Hans/settings.json +++ b/apps/dokploy/public/locales/zh-Hans/settings.json @@ -1,58 +1,67 @@ { - "settings.common.save": "保存", - "settings.common.enterTerminal": "终端", - "settings.server.domain.title": "服务器域名", - "settings.server.domain.description": "为您的服务器应用添加域名。", - "settings.server.domain.form.domain": "域名", - "settings.server.domain.form.letsEncryptEmail": "Let's Encrypt 邮箱", - "settings.server.domain.form.certificate.label": "证书提供商", - "settings.server.domain.form.certificate.placeholder": "选择证书", - "settings.server.domain.form.certificateOptions.none": "无", - "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt", - - "settings.server.webServer.title": "Web 服务器", - "settings.server.webServer.description": "重载或清理 Web 服务器。", - "settings.server.webServer.actions": "操作", - "settings.server.webServer.reload": "重新加载", - "settings.server.webServer.watchLogs": "查看日志", - "settings.server.webServer.updateServerIp": "更新服务器 IP", - "settings.server.webServer.server.label": "服务器", - "settings.server.webServer.traefik.label": "Traefik", - "settings.server.webServer.traefik.modifyEnv": "修改环境变量", - "settings.server.webServer.traefik.managePorts": "额外端口映射", - "settings.server.webServer.traefik.managePortsDescription": "为 Traefik 添加或删除额外端口", - "settings.server.webServer.traefik.targetPort": "目标端口", - "settings.server.webServer.traefik.publishedPort": "发布端口", - "settings.server.webServer.traefik.addPort": "添加端口", - "settings.server.webServer.traefik.portsUpdated": "端口更新成功", - "settings.server.webServer.traefik.portsUpdateError": "端口更新失败", - "settings.server.webServer.traefik.publishMode": "发布模式", - "settings.server.webServer.storage.label": "存储空间", - "settings.server.webServer.storage.cleanUnusedImages": "清理未使用的镜像", - "settings.server.webServer.storage.cleanUnusedVolumes": "清理未使用的卷", - "settings.server.webServer.storage.cleanStoppedContainers": "清理已停止的容器", - "settings.server.webServer.storage.cleanDockerBuilder": "清理 Docker Builder 和系统", - "settings.server.webServer.storage.cleanMonitoring": "清理监控数据", - "settings.server.webServer.storage.cleanAll": "清理所有内容", - - "settings.profile.title": "账户", - "settings.profile.description": "在此更改您的个人资料详情。", - "settings.profile.email": "邮箱", - "settings.profile.password": "密码", - "settings.profile.avatar": "头像", - - "settings.appearance.title": "外观", - "settings.appearance.description": "自定义您的仪表盘主题。", - "settings.appearance.theme": "主题", - "settings.appearance.themeDescription": "为您的仪表盘选择主题", - "settings.appearance.themes.light": "明亮", - "settings.appearance.themes.dark": "暗黑", - "settings.appearance.themes.system": "跟随系统", - "settings.appearance.language": "语言", - "settings.appearance.languageDescription": "为您的仪表盘选择语言", - - "settings.terminal.connectionSettings": "连接设置", - "settings.terminal.ipAddress": "IP 地址", - "settings.terminal.port": "端口", - "settings.terminal.username": "用户名" -} + "settings.common.save": "保存", + "settings.common.enterTerminal": "终端", + "settings.server.domain.title": "服务器域名", + "settings.server.domain.description": "为您的服务器应用添加域名。", + "settings.server.domain.form.domain": "域名", + "settings.server.domain.form.letsEncryptEmail": "Let's Encrypt 邮箱", + "settings.server.domain.form.certificate.label": "证书提供商", + "settings.server.domain.form.certificate.placeholder": "选择证书", + "settings.server.domain.form.certificateOptions.none": "无", + "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt", + "settings.server.webServer.title": "Web 服务器", + "settings.server.webServer.description": "重载或清理 Web 服务器。", + "settings.server.webServer.actions": "操作", + "settings.server.webServer.reload": "重新加载", + "settings.server.webServer.watchLogs": "查看日志", + "settings.server.webServer.updateServerIp": "更新服务器 IP", + "settings.server.webServer.server.label": "服务器", + "settings.server.webServer.traefik.label": "Traefik", + "settings.server.webServer.traefik.modifyEnv": "修改环境变量", + "settings.server.webServer.traefik.managePorts": "额外端口映射", + "settings.server.webServer.traefik.managePortsDescription": "为 Traefik 添加或删除额外端口", + "settings.server.webServer.traefik.targetPort": "目标端口", + "settings.server.webServer.traefik.publishedPort": "发布端口", + "settings.server.webServer.traefik.addPort": "添加端口", + "settings.server.webServer.traefik.portsUpdated": "端口更新成功", + "settings.server.webServer.traefik.portsUpdateError": "端口更新失败", + "settings.server.webServer.traefik.publishMode": "发布模式", + "settings.server.webServer.storage.label": "存储空间", + "settings.server.webServer.storage.cleanUnusedImages": "清理未使用的镜像", + "settings.server.webServer.storage.cleanUnusedVolumes": "清理未使用的卷", + "settings.server.webServer.storage.cleanStoppedContainers": "清理已停止的容器", + "settings.server.webServer.storage.cleanDockerBuilder": "清理 Docker Builder 和系统", + "settings.server.webServer.storage.cleanMonitoring": "清理监控数据", + "settings.server.webServer.storage.cleanAll": "清理所有内容", + "settings.profile.title": "账户", + "settings.profile.description": "在此更改您的个人资料详情。", + "settings.profile.email": "邮箱", + "settings.profile.password": "密码", + "settings.profile.avatar": "头像", + "settings.appearance.title": "外观", + "settings.appearance.description": "自定义您的仪表盘主题。", + "settings.appearance.theme": "主题", + "settings.appearance.themeDescription": "为您的仪表盘选择主题", + "settings.appearance.themes.light": "明亮", + "settings.appearance.themes.dark": "暗黑", + "settings.appearance.themes.system": "跟随系统", + "settings.appearance.language": "语言", + "settings.appearance.languageDescription": "为您的仪表盘选择语言", + "settings.terminal.connectionSettings": "连接设置", + "settings.terminal.ipAddress": "IP 地址", + "settings.terminal.port": "端口", + "settings.terminal.username": "用户名", + "settings.settings": "设置", + "settings.general": "通用设置", + "settings.security": "安全", + "settings.users": "用户管理", + "settings.roles": "角色管理", + "settings.permissions": "权限", + "settings.api": "API设置", + "settings.certificates": "证书管理", + "settings.ssh": "SSH密钥", + "settings.backups": "备份", + "settings.logs": "日志", + "settings.updates": "更新", + "settings.network": "网络" +} \ No newline at end of file diff --git a/extract.js b/extract.js new file mode 100644 index 000000000..61d5d2d7f --- /dev/null +++ b/extract.js @@ -0,0 +1,270 @@ +console.log('Creating translation extractor script...'); + +const fs = require('fs'); +const path = require('path'); + +// 存储找到的所有翻译键 +const translationKeys = { + common: new Set(), + settings: new Set() +}; + +// 匹配更多格式的翻译函数调用 +// 支持 t('common.xxx')、t("common.xxx")、t(`common.xxx`) +const translationPatterns = [ + /t\(\s*['"]([a-zA-Z0-9._-]+)['"]?\s*[,)]/g, // t('key') 或 t("key") + /t\(\s*`([a-zA-Z0-9._-]+)`\s*[,)]/g, // t(`key`) + /useTranslation\(\s*["']([a-zA-Z0-9._-]+)["']\s*\)/g, // useTranslation('namespace') + /serverSideTranslations\([^)]*["']([a-zA-Z0-9._-]+)["']/g // serverSideTranslations(..., ['namespace']) +]; + +const namespaceRegex = /^(common|settings)\./; + +// 递归扫描目录下的所有 JS 和 TS 文件 +function scanDirectory(directory) { + try { + const entries = fs.readdirSync(directory, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(directory, entry.name); + + if (entry.isDirectory() && !fullPath.includes('node_modules') && !fullPath.includes('.next')) { + scanDirectory(fullPath); + } else if (entry.isFile() && /\.(js|jsx|ts|tsx)$/.test(entry.name)) { + try { + const content = fs.readFileSync(fullPath, 'utf8'); + + // 检查文件中是否使用了翻译 + let usesTranslation = false; + if (content.includes('useTranslation') || content.includes('t(') || content.includes('serverSideTranslations')) { + usesTranslation = true; + } + + if (usesTranslation) { + // 使用所有模式匹配翻译键 + for (const pattern of translationPatterns) { + let match; + while ((match = pattern.exec(content)) !== null) { + const key = match[1]; + + // 检查是否有命名空间 + const namespaceMatch = key.match(namespaceRegex); + if (namespaceMatch) { + const namespace = namespaceMatch[1]; + if (namespace === 'common' || namespace === 'settings') { + translationKeys[namespace].add(key); + } + } + + // 如果文件中导入了特定命名空间,所有的 t(key) 都属于该命名空间 + if (content.includes(`useTranslation('common')`) || content.includes(`useTranslation("common")`)) { + if (!key.includes('.')) { + translationKeys.common.add(`common.${key}`); + } + } + + if (content.includes(`useTranslation('settings')`) || content.includes(`useTranslation("settings")`)) { + if (!key.includes('.')) { + translationKeys.settings.add(`settings.${key}`); + } + } + } + } + + // 控制台输出被处理的文件及其找到的翻译键 + if (usesTranslation) { + console.log(`检查文件: ${fullPath}`); + } + } + } catch (error) { + console.error(`Error reading file ${fullPath}:`, error); + } + } + } + } catch (error) { + console.error(`Error scanning directory ${directory}:`, error); + } +} + +// 手动添加一些常见的翻译键(基于直接观察和常见用法) +function addCommonTranslationKeys() { + const commonKeys = [ + 'dashboard.title', 'dashboard.overview', 'dashboard.projects', 'dashboard.servers', + 'dashboard.docker', 'dashboard.monitoring', 'dashboard.settings', 'dashboard.logout', + 'dashboard.profile', 'dashboard.terminal', 'dashboard.containers', 'dashboard.images', + 'dashboard.volumes', 'dashboard.networks', + + 'button.create', 'button.edit', 'button.delete', 'button.cancel', + 'button.save', 'button.confirm', 'button.back', 'button.next', 'button.finish', + + 'status.running', 'status.stopped', 'status.error', 'status.pending', + 'status.success', 'status.failed', + + 'form.required', 'form.invalid', 'form.submit', 'form.reset', + + 'notification.success', 'notification.error', 'notification.warning', 'notification.info', + + 'time.now', 'time.minutes', 'time.hours', 'time.days', + + 'filter.all', 'filter.active', 'filter.inactive', + + 'sort.asc', 'sort.desc', + + 'search.placeholder', 'search.noResults', + + 'pagination.prev', 'pagination.next', 'pagination.of', + + 'error.notFound', 'error.serverError', 'error.unauthorized', 'error.forbidden', + + 'loading', 'empty', 'more', 'less', + + 'project.create', 'project.edit', 'project.delete', 'project.name', 'project.description', + + 'service.create', 'service.edit', 'service.delete', 'service.name', 'service.type', + + 'domain.add', 'domain.remove', + + 'environment.variables', 'environment.add', 'environment.edit', + 'environment.name', 'environment.value' + ]; + + commonKeys.forEach(key => { + translationKeys.common.add(`common.${key}`); + }); +} + +// 读取现有翻译文件 +function readTranslationFile(filePath) { + try { + if (fs.existsSync(filePath)) { + const content = fs.readFileSync(filePath, 'utf8'); + return JSON.parse(content); + } + } catch (error) { + console.error(`Error reading translation file ${filePath}:`, error); + } + return {}; +} + +// 主函数 +function extractTranslations() { + const appsDir = path.join(__dirname, 'apps', 'dokploy'); + + // 扫描代码库 + scanDirectory(appsDir); + + // 手动添加常见的翻译键 + addCommonTranslationKeys(); + + // 读取现有翻译文件 + const zhHansCommonPath = path.join(appsDir, 'public', 'locales', 'zh-Hans', 'common.json'); + const zhHansSettingsPath = path.join(appsDir, 'public', 'locales', 'zh-Hans', 'settings.json'); + + const existingCommon = readTranslationFile(zhHansCommonPath); + const existingSettings = readTranslationFile(zhHansSettingsPath); + + // 准备新的翻译文件 + const newCommon = {}; + const newSettings = {}; + + // 处理 common 命名空间 + for (const key of translationKeys.common) { + const shortKey = key.replace('common.', ''); + newCommon[key] = existingCommon[key] || `[需要翻译] ${shortKey}`; + } + + // 处理 settings 命名空间 + for (const key of translationKeys.settings) { + const shortKey = key.replace('settings.', ''); + newSettings[key] = existingSettings[key] || `[需要翻译] ${shortKey}`; + } + + // 输出结果 + console.log('=== 提取的 common 翻译键 ==='); + console.log(Array.from(translationKeys.common).sort().join('\n')); + console.log(`\n共找到 ${translationKeys.common.size} 个 common 翻译键`); + + console.log('\n=== 提取的 settings 翻译键 ==='); + console.log(Array.from(translationKeys.settings).sort().join('\n')); + console.log(`\n共找到 ${translationKeys.settings.size} 个 settings 翻译键`); + + // 创建包含缺失翻译的新文件 + const missingCommonTranslations = {}; + const missingSettingsTranslations = {}; + + for (const key of translationKeys.common) { + if (!existingCommon[key]) { + const shortKey = key.replace('common.', ''); + missingCommonTranslations[key] = `[需要翻译] ${shortKey}`; + } + } + + for (const key of translationKeys.settings) { + if (!existingSettings[key]) { + const shortKey = key.replace('settings.', ''); + missingSettingsTranslations[key] = `[需要翻译] ${shortKey}`; + } + } + + // 输出缺失的翻译 + console.log('\n=== 缺失的 common 翻译 ==='); + console.log(JSON.stringify(missingCommonTranslations, null, 2)); + console.log(`\n共缺失 ${Object.keys(missingCommonTranslations).length} 个 common 翻译`); + + console.log('\n=== 缺失的 settings 翻译 ==='); + console.log(JSON.stringify(missingSettingsTranslations, null, 2)); + console.log(`\n共缺失 ${Object.keys(missingSettingsTranslations).length} 个 settings 翻译`); + + // 输出可以直接复制到文件中的完整翻译对象 + console.log('\n=== 完整的 common.json 内容 ==='); + const fullCommon = { ...existingCommon }; + translationKeys.common.forEach(key => { + if (!fullCommon[key]) { + const shortKey = key.replace('common.', ''); + fullCommon[key] = `[翻译] ${shortKey}`; + } + }); + console.log(JSON.stringify(fullCommon, null, 2)); + + console.log('\n=== 完整的 settings.json 内容 ==='); + const fullSettings = { ...existingSettings }; + translationKeys.settings.forEach(key => { + if (!fullSettings[key]) { + const shortKey = key.replace('settings.', ''); + fullSettings[key] = `[翻译] ${shortKey}`; + } + }); + console.log(JSON.stringify(fullSettings, null, 2)); + + // 优化生成的翻译文件格式:移除命名空间前缀 + const optimizedCommon = {}; + Object.keys(fullCommon).forEach(key => { + const shortKey = key.replace('common.', ''); + optimizedCommon[shortKey] = fullCommon[key]; + }); + + const optimizedSettings = {}; + Object.keys(fullSettings).forEach(key => { + const shortKey = key.replace('settings.', ''); + optimizedSettings[shortKey] = fullSettings[key]; + }); + + // 写入文件 + fs.writeFileSync('missing-common-translations.json', JSON.stringify(missingCommonTranslations, null, 2), 'utf8'); + fs.writeFileSync('missing-settings-translations.json', JSON.stringify(missingSettingsTranslations, null, 2), 'utf8'); + fs.writeFileSync('full-common-translations.json', JSON.stringify(fullCommon, null, 2), 'utf8'); + fs.writeFileSync('full-settings-translations.json', JSON.stringify(fullSettings, null, 2), 'utf8'); + fs.writeFileSync('optimized-common-translations.json', JSON.stringify(optimizedCommon, null, 2), 'utf8'); + fs.writeFileSync('optimized-settings-translations.json', JSON.stringify(optimizedSettings, null, 2), 'utf8'); + + console.log('\n翻译提取完成!'); + console.log('文件已保存:'); + console.log('- missing-common-translations.json: 缺失的 common 翻译'); + console.log('- missing-settings-translations.json: 缺失的 settings 翻译'); + console.log('- full-common-translations.json: 完整的 common 翻译(包含命名空间)'); + console.log('- full-settings-translations.json: 完整的 settings 翻译(包含命名空间)'); + console.log('- optimized-common-translations.json: 优化格式的 common 翻译(不含命名空间)'); + console.log('- optimized-settings-translations.json: 优化格式的 settings 翻译(不含命名空间)'); +} + +extractTranslations(); diff --git a/merge-translations.js b/merge-translations.js new file mode 100644 index 000000000..441ebed49 --- /dev/null +++ b/merge-translations.js @@ -0,0 +1,252 @@ +// 读取已创建的翻译文件 +const fs = require('fs'); +const path = require('path'); + +try { + const basePath = process.cwd(); + const commonPart1Path = path.join(basePath, 'common-zh-Hans.json'); + const commonPart2Path = path.join(basePath, 'common-zh-Hans-buttons.json'); + const fullCommonPath = path.join(basePath, 'optimized-common-translations.json'); + const fullSettingsPath = path.join(basePath, 'optimized-settings-translations.json'); + + const commonPart1 = JSON.parse(fs.readFileSync(commonPart1Path, 'utf8')); + const commonPart2 = JSON.parse(fs.readFileSync(commonPart2Path, 'utf8')); + const fullCommon = JSON.parse(fs.readFileSync(fullCommonPath, 'utf8')); + const fullSettings = JSON.parse(fs.readFileSync(fullSettingsPath, 'utf8')); + + // 创建一个全新的翻译对象 + const mergedCommon = {}; + const mergedSettings = {}; + + // 添加通用组件翻译 + mergedCommon["dashboard.title"] = "仪表盘"; + mergedCommon["dashboard.overview"] = "概览"; + mergedCommon["dashboard.projects"] = "项目"; + mergedCommon["dashboard.servers"] = "服务器"; + mergedCommon["dashboard.docker"] = "Docker"; + mergedCommon["dashboard.monitoring"] = "监控"; + mergedCommon["dashboard.settings"] = "设置"; + mergedCommon["dashboard.logout"] = "退出登录"; + mergedCommon["dashboard.profile"] = "个人资料"; + mergedCommon["dashboard.terminal"] = "终端"; + mergedCommon["dashboard.containers"] = "容器"; + mergedCommon["dashboard.images"] = "镜像"; + mergedCommon["dashboard.volumes"] = "卷"; + mergedCommon["dashboard.networks"] = "网络"; + + // 按钮翻译 + mergedCommon["button.create"] = "创建"; + mergedCommon["button.edit"] = "编辑"; + mergedCommon["button.delete"] = "删除"; + mergedCommon["button.cancel"] = "取消"; + mergedCommon["button.save"] = "保存"; + mergedCommon["button.confirm"] = "确认"; + mergedCommon["button.back"] = "返回"; + mergedCommon["button.next"] = "下一步"; + mergedCommon["button.finish"] = "完成"; + + // 状态翻译 + mergedCommon["status.running"] = "运行中"; + mergedCommon["status.stopped"] = "已停止"; + mergedCommon["status.error"] = "错误"; + mergedCommon["status.pending"] = "等待中"; + mergedCommon["status.success"] = "成功"; + mergedCommon["status.failed"] = "失败"; + + // 表单翻译 + mergedCommon["form.required"] = "必填"; + mergedCommon["form.invalid"] = "无效"; + mergedCommon["form.submit"] = "提交"; + mergedCommon["form.reset"] = "重置"; + + // 通知翻译 + mergedCommon["notification.success"] = "操作成功"; + mergedCommon["notification.error"] = "操作失败"; + mergedCommon["notification.warning"] = "警告"; + mergedCommon["notification.info"] = "信息"; + + // 时间翻译 + mergedCommon["time.now"] = "刚刚"; + mergedCommon["time.minutes"] = "分钟前"; + mergedCommon["time.hours"] = "小时前"; + mergedCommon["time.days"] = "天前"; + + // 过滤翻译 + mergedCommon["filter.all"] = "全部"; + mergedCommon["filter.active"] = "活跃"; + mergedCommon["filter.inactive"] = "不活跃"; + + // 排序翻译 + mergedCommon["sort.asc"] = "升序"; + mergedCommon["sort.desc"] = "降序"; + + // 搜索翻译 + mergedCommon["search.placeholder"] = "搜索..."; + mergedCommon["search.noResults"] = "无结果"; + + // 分页翻译 + mergedCommon["pagination.prev"] = "上一页"; + mergedCommon["pagination.next"] = "下一页"; + mergedCommon["pagination.of"] = "共 {0} 页"; + + // 错误翻译 + mergedCommon["error.notFound"] = "未找到"; + mergedCommon["error.serverError"] = "服务器错误"; + mergedCommon["error.unauthorized"] = "未授权"; + mergedCommon["error.forbidden"] = "禁止访问"; + + // 通用状态翻译 + mergedCommon["loading"] = "加载中..."; + mergedCommon["empty"] = "暂无数据"; + mergedCommon["more"] = "更多"; + mergedCommon["less"] = "收起"; + + // 项目翻译 + mergedCommon["project.create"] = "创建项目"; + mergedCommon["project.edit"] = "编辑项目"; + mergedCommon["project.delete"] = "删除项目"; + mergedCommon["project.name"] = "项目名称"; + mergedCommon["project.description"] = "项目描述"; + + // 服务翻译 + mergedCommon["service.create"] = "创建服务"; + mergedCommon["service.edit"] = "编辑服务"; + mergedCommon["service.delete"] = "删除服务"; + mergedCommon["service.name"] = "服务名称"; + mergedCommon["service.type"] = "服务类型"; + + // 域名翻译 + mergedCommon["domain.add"] = "添加域名"; + mergedCommon["domain.remove"] = "移除域名"; + + // 环境变量翻译 + mergedCommon["environment.variables"] = "环境变量"; + mergedCommon["environment.add"] = "添加环境变量"; + mergedCommon["environment.edit"] = "编辑环境变量"; + mergedCommon["environment.name"] = "变量名"; + mergedCommon["environment.value"] = "变量值"; + + // 设置页面的通用翻译 + mergedSettings["common.save"] = "保存"; + mergedSettings["common.enterTerminal"] = "终端"; + + // 服务器域名设置 + mergedSettings["server.domain.title"] = "服务器域名"; + mergedSettings["server.domain.description"] = "为您的服务器应用添加域名。"; + mergedSettings["server.domain.form.domain"] = "域名"; + mergedSettings["server.domain.form.letsEncryptEmail"] = "Let's Encrypt 邮箱"; + mergedSettings["server.domain.form.certificate.label"] = "证书提供商"; + mergedSettings["server.domain.form.certificate.placeholder"] = "选择证书"; + mergedSettings["server.domain.form.certificateOptions.none"] = "无"; + mergedSettings["server.domain.form.certificateOptions.letsencrypt"] = "Let's Encrypt"; + + // Web服务器设置 + mergedSettings["server.webServer.title"] = "Web 服务器"; + mergedSettings["server.webServer.description"] = "重载或清理 Web 服务器。"; + mergedSettings["server.webServer.actions"] = "操作"; + mergedSettings["server.webServer.reload"] = "重新加载"; + mergedSettings["server.webServer.watchLogs"] = "查看日志"; + mergedSettings["server.webServer.updateServerIp"] = "更新服务器 IP"; + mergedSettings["server.webServer.server.label"] = "服务器"; + + // Traefik设置 + mergedSettings["server.webServer.traefik.label"] = "Traefik"; + mergedSettings["server.webServer.traefik.modifyEnv"] = "修改环境变量"; + mergedSettings["server.webServer.traefik.managePorts"] = "额外端口映射"; + mergedSettings["server.webServer.traefik.managePortsDescription"] = "为 Traefik 添加或删除额外端口"; + mergedSettings["server.webServer.traefik.targetPort"] = "目标端口"; + mergedSettings["server.webServer.traefik.publishedPort"] = "发布端口"; + mergedSettings["server.webServer.traefik.addPort"] = "添加端口"; + mergedSettings["server.webServer.traefik.portsUpdated"] = "端口更新成功"; + mergedSettings["server.webServer.traefik.portsUpdateError"] = "端口更新失败"; + mergedSettings["server.webServer.traefik.publishMode"] = "发布模式"; + + // 存储空间设置 + mergedSettings["server.webServer.storage.label"] = "存储空间"; + mergedSettings["server.webServer.storage.cleanUnusedImages"] = "清理未使用的镜像"; + mergedSettings["server.webServer.storage.cleanUnusedVolumes"] = "清理未使用的卷"; + mergedSettings["server.webServer.storage.cleanStoppedContainers"] = "清理已停止的容器"; + mergedSettings["server.webServer.storage.cleanDockerBuilder"] = "清理 Docker Builder 和系统"; + mergedSettings["server.webServer.storage.cleanMonitoring"] = "清理监控数据"; + mergedSettings["server.webServer.storage.cleanAll"] = "清理所有内容"; + + // 个人资料设置 + mergedSettings["profile.title"] = "账户"; + mergedSettings["profile.description"] = "在此更改您的个人资料详情。"; + mergedSettings["profile.email"] = "邮箱"; + mergedSettings["profile.password"] = "密码"; + mergedSettings["profile.avatar"] = "头像"; + + // 外观设置 + mergedSettings["appearance.title"] = "外观"; + mergedSettings["appearance.description"] = "自定义您的仪表盘主题。"; + mergedSettings["appearance.theme"] = "主题"; + mergedSettings["appearance.themeDescription"] = "为您的仪表盘选择主题"; + mergedSettings["appearance.themes.light"] = "明亮"; + mergedSettings["appearance.themes.dark"] = "暗黑"; + mergedSettings["appearance.themes.system"] = "跟随系统"; + mergedSettings["appearance.language"] = "语言"; + mergedSettings["appearance.languageDescription"] = "为您的仪表盘选择语言"; + + // 终端设置 + mergedSettings["terminal.connectionSettings"] = "连接设置"; + mergedSettings["terminal.ipAddress"] = "IP 地址"; + mergedSettings["terminal.port"] = "端口"; + mergedSettings["terminal.username"] = "用户名"; + + // 其他设置 + mergedSettings["settings"] = "设置"; + mergedSettings["general"] = "通用设置"; + mergedSettings["security"] = "安全"; + mergedSettings["users"] = "用户管理"; + mergedSettings["roles"] = "角色管理"; + mergedSettings["permissions"] = "权限"; + mergedSettings["api"] = "API设置"; + mergedSettings["certificates"] = "证书管理"; + mergedSettings["ssh"] = "SSH密钥"; + mergedSettings["backups"] = "备份"; + mergedSettings["logs"] = "日志"; + mergedSettings["updates"] = "更新"; + mergedSettings["network"] = "网络"; + + // 输出合并后的文件内容 + console.log('Common translations total:', Object.keys(mergedCommon).length); + console.log('Settings translations total:', Object.keys(mergedSettings).length); + + // 保存为最终的翻译文件 + fs.writeFileSync(path.join(basePath, 'final-zh-Hans-common.json'), JSON.stringify(mergedCommon, null, 2)); + fs.writeFileSync(path.join(basePath, 'final-zh-Hans-settings.json'), JSON.stringify(mergedSettings, null, 2)); + + // 输出翻译完成的统计 + const commonKeys = Object.keys(mergedCommon); + const settingsKeys = Object.keys(mergedSettings); + console.log('最终翻译文件已保存:'); + console.log(`- 通用翻译 (${commonKeys.length} 个词条)`); + console.log(`- 设置翻译 (${settingsKeys.length} 个词条)`); + + // 创建最终放入项目中的文件(按项目结构) + const projectCommonPath = path.join(basePath, 'apps', 'dokploy', 'public', 'locales', 'zh-Hans'); + + // 确保目录存在 + if (!fs.existsSync(projectCommonPath)) { + fs.mkdirSync(projectCommonPath, { recursive: true }); + console.log(`创建目录: ${projectCommonPath}`); + } + + // 写入到项目中的目标位置 + const projectCommonFilePath = path.join(projectCommonPath, 'common.json'); + const projectSettingsFilePath = path.join(projectCommonPath, 'settings.json'); + + console.log(`尝试写入到:\n- ${projectCommonFilePath}\n- ${projectSettingsFilePath}`); + + try { + fs.writeFileSync(projectCommonFilePath, JSON.stringify(mergedCommon, null, 2)); + fs.writeFileSync(projectSettingsFilePath, JSON.stringify(mergedSettings, null, 2)); + console.log('已成功写入到项目文件夹中!'); + } catch (error) { + console.error('写入到项目文件夹失败:', error.message); + console.log('请手动将文件复制到目标位置。'); + } +} catch (error) { + console.error('错误:', error); +} From 350bed217c5574ed829999a6a9996770dee36227 Mon Sep 17 00:00:00 2001 From: Hoofei Date: Sun, 6 Apr 2025 12:37:52 +0800 Subject: [PATCH 18/53] Delete the extract script --- extract.js | 270 ------------------------------------------ merge-translations.js | 252 --------------------------------------- 2 files changed, 522 deletions(-) delete mode 100644 extract.js delete mode 100644 merge-translations.js diff --git a/extract.js b/extract.js deleted file mode 100644 index 61d5d2d7f..000000000 --- a/extract.js +++ /dev/null @@ -1,270 +0,0 @@ -console.log('Creating translation extractor script...'); - -const fs = require('fs'); -const path = require('path'); - -// 存储找到的所有翻译键 -const translationKeys = { - common: new Set(), - settings: new Set() -}; - -// 匹配更多格式的翻译函数调用 -// 支持 t('common.xxx')、t("common.xxx")、t(`common.xxx`) -const translationPatterns = [ - /t\(\s*['"]([a-zA-Z0-9._-]+)['"]?\s*[,)]/g, // t('key') 或 t("key") - /t\(\s*`([a-zA-Z0-9._-]+)`\s*[,)]/g, // t(`key`) - /useTranslation\(\s*["']([a-zA-Z0-9._-]+)["']\s*\)/g, // useTranslation('namespace') - /serverSideTranslations\([^)]*["']([a-zA-Z0-9._-]+)["']/g // serverSideTranslations(..., ['namespace']) -]; - -const namespaceRegex = /^(common|settings)\./; - -// 递归扫描目录下的所有 JS 和 TS 文件 -function scanDirectory(directory) { - try { - const entries = fs.readdirSync(directory, { withFileTypes: true }); - - for (const entry of entries) { - const fullPath = path.join(directory, entry.name); - - if (entry.isDirectory() && !fullPath.includes('node_modules') && !fullPath.includes('.next')) { - scanDirectory(fullPath); - } else if (entry.isFile() && /\.(js|jsx|ts|tsx)$/.test(entry.name)) { - try { - const content = fs.readFileSync(fullPath, 'utf8'); - - // 检查文件中是否使用了翻译 - let usesTranslation = false; - if (content.includes('useTranslation') || content.includes('t(') || content.includes('serverSideTranslations')) { - usesTranslation = true; - } - - if (usesTranslation) { - // 使用所有模式匹配翻译键 - for (const pattern of translationPatterns) { - let match; - while ((match = pattern.exec(content)) !== null) { - const key = match[1]; - - // 检查是否有命名空间 - const namespaceMatch = key.match(namespaceRegex); - if (namespaceMatch) { - const namespace = namespaceMatch[1]; - if (namespace === 'common' || namespace === 'settings') { - translationKeys[namespace].add(key); - } - } - - // 如果文件中导入了特定命名空间,所有的 t(key) 都属于该命名空间 - if (content.includes(`useTranslation('common')`) || content.includes(`useTranslation("common")`)) { - if (!key.includes('.')) { - translationKeys.common.add(`common.${key}`); - } - } - - if (content.includes(`useTranslation('settings')`) || content.includes(`useTranslation("settings")`)) { - if (!key.includes('.')) { - translationKeys.settings.add(`settings.${key}`); - } - } - } - } - - // 控制台输出被处理的文件及其找到的翻译键 - if (usesTranslation) { - console.log(`检查文件: ${fullPath}`); - } - } - } catch (error) { - console.error(`Error reading file ${fullPath}:`, error); - } - } - } - } catch (error) { - console.error(`Error scanning directory ${directory}:`, error); - } -} - -// 手动添加一些常见的翻译键(基于直接观察和常见用法) -function addCommonTranslationKeys() { - const commonKeys = [ - 'dashboard.title', 'dashboard.overview', 'dashboard.projects', 'dashboard.servers', - 'dashboard.docker', 'dashboard.monitoring', 'dashboard.settings', 'dashboard.logout', - 'dashboard.profile', 'dashboard.terminal', 'dashboard.containers', 'dashboard.images', - 'dashboard.volumes', 'dashboard.networks', - - 'button.create', 'button.edit', 'button.delete', 'button.cancel', - 'button.save', 'button.confirm', 'button.back', 'button.next', 'button.finish', - - 'status.running', 'status.stopped', 'status.error', 'status.pending', - 'status.success', 'status.failed', - - 'form.required', 'form.invalid', 'form.submit', 'form.reset', - - 'notification.success', 'notification.error', 'notification.warning', 'notification.info', - - 'time.now', 'time.minutes', 'time.hours', 'time.days', - - 'filter.all', 'filter.active', 'filter.inactive', - - 'sort.asc', 'sort.desc', - - 'search.placeholder', 'search.noResults', - - 'pagination.prev', 'pagination.next', 'pagination.of', - - 'error.notFound', 'error.serverError', 'error.unauthorized', 'error.forbidden', - - 'loading', 'empty', 'more', 'less', - - 'project.create', 'project.edit', 'project.delete', 'project.name', 'project.description', - - 'service.create', 'service.edit', 'service.delete', 'service.name', 'service.type', - - 'domain.add', 'domain.remove', - - 'environment.variables', 'environment.add', 'environment.edit', - 'environment.name', 'environment.value' - ]; - - commonKeys.forEach(key => { - translationKeys.common.add(`common.${key}`); - }); -} - -// 读取现有翻译文件 -function readTranslationFile(filePath) { - try { - if (fs.existsSync(filePath)) { - const content = fs.readFileSync(filePath, 'utf8'); - return JSON.parse(content); - } - } catch (error) { - console.error(`Error reading translation file ${filePath}:`, error); - } - return {}; -} - -// 主函数 -function extractTranslations() { - const appsDir = path.join(__dirname, 'apps', 'dokploy'); - - // 扫描代码库 - scanDirectory(appsDir); - - // 手动添加常见的翻译键 - addCommonTranslationKeys(); - - // 读取现有翻译文件 - const zhHansCommonPath = path.join(appsDir, 'public', 'locales', 'zh-Hans', 'common.json'); - const zhHansSettingsPath = path.join(appsDir, 'public', 'locales', 'zh-Hans', 'settings.json'); - - const existingCommon = readTranslationFile(zhHansCommonPath); - const existingSettings = readTranslationFile(zhHansSettingsPath); - - // 准备新的翻译文件 - const newCommon = {}; - const newSettings = {}; - - // 处理 common 命名空间 - for (const key of translationKeys.common) { - const shortKey = key.replace('common.', ''); - newCommon[key] = existingCommon[key] || `[需要翻译] ${shortKey}`; - } - - // 处理 settings 命名空间 - for (const key of translationKeys.settings) { - const shortKey = key.replace('settings.', ''); - newSettings[key] = existingSettings[key] || `[需要翻译] ${shortKey}`; - } - - // 输出结果 - console.log('=== 提取的 common 翻译键 ==='); - console.log(Array.from(translationKeys.common).sort().join('\n')); - console.log(`\n共找到 ${translationKeys.common.size} 个 common 翻译键`); - - console.log('\n=== 提取的 settings 翻译键 ==='); - console.log(Array.from(translationKeys.settings).sort().join('\n')); - console.log(`\n共找到 ${translationKeys.settings.size} 个 settings 翻译键`); - - // 创建包含缺失翻译的新文件 - const missingCommonTranslations = {}; - const missingSettingsTranslations = {}; - - for (const key of translationKeys.common) { - if (!existingCommon[key]) { - const shortKey = key.replace('common.', ''); - missingCommonTranslations[key] = `[需要翻译] ${shortKey}`; - } - } - - for (const key of translationKeys.settings) { - if (!existingSettings[key]) { - const shortKey = key.replace('settings.', ''); - missingSettingsTranslations[key] = `[需要翻译] ${shortKey}`; - } - } - - // 输出缺失的翻译 - console.log('\n=== 缺失的 common 翻译 ==='); - console.log(JSON.stringify(missingCommonTranslations, null, 2)); - console.log(`\n共缺失 ${Object.keys(missingCommonTranslations).length} 个 common 翻译`); - - console.log('\n=== 缺失的 settings 翻译 ==='); - console.log(JSON.stringify(missingSettingsTranslations, null, 2)); - console.log(`\n共缺失 ${Object.keys(missingSettingsTranslations).length} 个 settings 翻译`); - - // 输出可以直接复制到文件中的完整翻译对象 - console.log('\n=== 完整的 common.json 内容 ==='); - const fullCommon = { ...existingCommon }; - translationKeys.common.forEach(key => { - if (!fullCommon[key]) { - const shortKey = key.replace('common.', ''); - fullCommon[key] = `[翻译] ${shortKey}`; - } - }); - console.log(JSON.stringify(fullCommon, null, 2)); - - console.log('\n=== 完整的 settings.json 内容 ==='); - const fullSettings = { ...existingSettings }; - translationKeys.settings.forEach(key => { - if (!fullSettings[key]) { - const shortKey = key.replace('settings.', ''); - fullSettings[key] = `[翻译] ${shortKey}`; - } - }); - console.log(JSON.stringify(fullSettings, null, 2)); - - // 优化生成的翻译文件格式:移除命名空间前缀 - const optimizedCommon = {}; - Object.keys(fullCommon).forEach(key => { - const shortKey = key.replace('common.', ''); - optimizedCommon[shortKey] = fullCommon[key]; - }); - - const optimizedSettings = {}; - Object.keys(fullSettings).forEach(key => { - const shortKey = key.replace('settings.', ''); - optimizedSettings[shortKey] = fullSettings[key]; - }); - - // 写入文件 - fs.writeFileSync('missing-common-translations.json', JSON.stringify(missingCommonTranslations, null, 2), 'utf8'); - fs.writeFileSync('missing-settings-translations.json', JSON.stringify(missingSettingsTranslations, null, 2), 'utf8'); - fs.writeFileSync('full-common-translations.json', JSON.stringify(fullCommon, null, 2), 'utf8'); - fs.writeFileSync('full-settings-translations.json', JSON.stringify(fullSettings, null, 2), 'utf8'); - fs.writeFileSync('optimized-common-translations.json', JSON.stringify(optimizedCommon, null, 2), 'utf8'); - fs.writeFileSync('optimized-settings-translations.json', JSON.stringify(optimizedSettings, null, 2), 'utf8'); - - console.log('\n翻译提取完成!'); - console.log('文件已保存:'); - console.log('- missing-common-translations.json: 缺失的 common 翻译'); - console.log('- missing-settings-translations.json: 缺失的 settings 翻译'); - console.log('- full-common-translations.json: 完整的 common 翻译(包含命名空间)'); - console.log('- full-settings-translations.json: 完整的 settings 翻译(包含命名空间)'); - console.log('- optimized-common-translations.json: 优化格式的 common 翻译(不含命名空间)'); - console.log('- optimized-settings-translations.json: 优化格式的 settings 翻译(不含命名空间)'); -} - -extractTranslations(); diff --git a/merge-translations.js b/merge-translations.js deleted file mode 100644 index 441ebed49..000000000 --- a/merge-translations.js +++ /dev/null @@ -1,252 +0,0 @@ -// 读取已创建的翻译文件 -const fs = require('fs'); -const path = require('path'); - -try { - const basePath = process.cwd(); - const commonPart1Path = path.join(basePath, 'common-zh-Hans.json'); - const commonPart2Path = path.join(basePath, 'common-zh-Hans-buttons.json'); - const fullCommonPath = path.join(basePath, 'optimized-common-translations.json'); - const fullSettingsPath = path.join(basePath, 'optimized-settings-translations.json'); - - const commonPart1 = JSON.parse(fs.readFileSync(commonPart1Path, 'utf8')); - const commonPart2 = JSON.parse(fs.readFileSync(commonPart2Path, 'utf8')); - const fullCommon = JSON.parse(fs.readFileSync(fullCommonPath, 'utf8')); - const fullSettings = JSON.parse(fs.readFileSync(fullSettingsPath, 'utf8')); - - // 创建一个全新的翻译对象 - const mergedCommon = {}; - const mergedSettings = {}; - - // 添加通用组件翻译 - mergedCommon["dashboard.title"] = "仪表盘"; - mergedCommon["dashboard.overview"] = "概览"; - mergedCommon["dashboard.projects"] = "项目"; - mergedCommon["dashboard.servers"] = "服务器"; - mergedCommon["dashboard.docker"] = "Docker"; - mergedCommon["dashboard.monitoring"] = "监控"; - mergedCommon["dashboard.settings"] = "设置"; - mergedCommon["dashboard.logout"] = "退出登录"; - mergedCommon["dashboard.profile"] = "个人资料"; - mergedCommon["dashboard.terminal"] = "终端"; - mergedCommon["dashboard.containers"] = "容器"; - mergedCommon["dashboard.images"] = "镜像"; - mergedCommon["dashboard.volumes"] = "卷"; - mergedCommon["dashboard.networks"] = "网络"; - - // 按钮翻译 - mergedCommon["button.create"] = "创建"; - mergedCommon["button.edit"] = "编辑"; - mergedCommon["button.delete"] = "删除"; - mergedCommon["button.cancel"] = "取消"; - mergedCommon["button.save"] = "保存"; - mergedCommon["button.confirm"] = "确认"; - mergedCommon["button.back"] = "返回"; - mergedCommon["button.next"] = "下一步"; - mergedCommon["button.finish"] = "完成"; - - // 状态翻译 - mergedCommon["status.running"] = "运行中"; - mergedCommon["status.stopped"] = "已停止"; - mergedCommon["status.error"] = "错误"; - mergedCommon["status.pending"] = "等待中"; - mergedCommon["status.success"] = "成功"; - mergedCommon["status.failed"] = "失败"; - - // 表单翻译 - mergedCommon["form.required"] = "必填"; - mergedCommon["form.invalid"] = "无效"; - mergedCommon["form.submit"] = "提交"; - mergedCommon["form.reset"] = "重置"; - - // 通知翻译 - mergedCommon["notification.success"] = "操作成功"; - mergedCommon["notification.error"] = "操作失败"; - mergedCommon["notification.warning"] = "警告"; - mergedCommon["notification.info"] = "信息"; - - // 时间翻译 - mergedCommon["time.now"] = "刚刚"; - mergedCommon["time.minutes"] = "分钟前"; - mergedCommon["time.hours"] = "小时前"; - mergedCommon["time.days"] = "天前"; - - // 过滤翻译 - mergedCommon["filter.all"] = "全部"; - mergedCommon["filter.active"] = "活跃"; - mergedCommon["filter.inactive"] = "不活跃"; - - // 排序翻译 - mergedCommon["sort.asc"] = "升序"; - mergedCommon["sort.desc"] = "降序"; - - // 搜索翻译 - mergedCommon["search.placeholder"] = "搜索..."; - mergedCommon["search.noResults"] = "无结果"; - - // 分页翻译 - mergedCommon["pagination.prev"] = "上一页"; - mergedCommon["pagination.next"] = "下一页"; - mergedCommon["pagination.of"] = "共 {0} 页"; - - // 错误翻译 - mergedCommon["error.notFound"] = "未找到"; - mergedCommon["error.serverError"] = "服务器错误"; - mergedCommon["error.unauthorized"] = "未授权"; - mergedCommon["error.forbidden"] = "禁止访问"; - - // 通用状态翻译 - mergedCommon["loading"] = "加载中..."; - mergedCommon["empty"] = "暂无数据"; - mergedCommon["more"] = "更多"; - mergedCommon["less"] = "收起"; - - // 项目翻译 - mergedCommon["project.create"] = "创建项目"; - mergedCommon["project.edit"] = "编辑项目"; - mergedCommon["project.delete"] = "删除项目"; - mergedCommon["project.name"] = "项目名称"; - mergedCommon["project.description"] = "项目描述"; - - // 服务翻译 - mergedCommon["service.create"] = "创建服务"; - mergedCommon["service.edit"] = "编辑服务"; - mergedCommon["service.delete"] = "删除服务"; - mergedCommon["service.name"] = "服务名称"; - mergedCommon["service.type"] = "服务类型"; - - // 域名翻译 - mergedCommon["domain.add"] = "添加域名"; - mergedCommon["domain.remove"] = "移除域名"; - - // 环境变量翻译 - mergedCommon["environment.variables"] = "环境变量"; - mergedCommon["environment.add"] = "添加环境变量"; - mergedCommon["environment.edit"] = "编辑环境变量"; - mergedCommon["environment.name"] = "变量名"; - mergedCommon["environment.value"] = "变量值"; - - // 设置页面的通用翻译 - mergedSettings["common.save"] = "保存"; - mergedSettings["common.enterTerminal"] = "终端"; - - // 服务器域名设置 - mergedSettings["server.domain.title"] = "服务器域名"; - mergedSettings["server.domain.description"] = "为您的服务器应用添加域名。"; - mergedSettings["server.domain.form.domain"] = "域名"; - mergedSettings["server.domain.form.letsEncryptEmail"] = "Let's Encrypt 邮箱"; - mergedSettings["server.domain.form.certificate.label"] = "证书提供商"; - mergedSettings["server.domain.form.certificate.placeholder"] = "选择证书"; - mergedSettings["server.domain.form.certificateOptions.none"] = "无"; - mergedSettings["server.domain.form.certificateOptions.letsencrypt"] = "Let's Encrypt"; - - // Web服务器设置 - mergedSettings["server.webServer.title"] = "Web 服务器"; - mergedSettings["server.webServer.description"] = "重载或清理 Web 服务器。"; - mergedSettings["server.webServer.actions"] = "操作"; - mergedSettings["server.webServer.reload"] = "重新加载"; - mergedSettings["server.webServer.watchLogs"] = "查看日志"; - mergedSettings["server.webServer.updateServerIp"] = "更新服务器 IP"; - mergedSettings["server.webServer.server.label"] = "服务器"; - - // Traefik设置 - mergedSettings["server.webServer.traefik.label"] = "Traefik"; - mergedSettings["server.webServer.traefik.modifyEnv"] = "修改环境变量"; - mergedSettings["server.webServer.traefik.managePorts"] = "额外端口映射"; - mergedSettings["server.webServer.traefik.managePortsDescription"] = "为 Traefik 添加或删除额外端口"; - mergedSettings["server.webServer.traefik.targetPort"] = "目标端口"; - mergedSettings["server.webServer.traefik.publishedPort"] = "发布端口"; - mergedSettings["server.webServer.traefik.addPort"] = "添加端口"; - mergedSettings["server.webServer.traefik.portsUpdated"] = "端口更新成功"; - mergedSettings["server.webServer.traefik.portsUpdateError"] = "端口更新失败"; - mergedSettings["server.webServer.traefik.publishMode"] = "发布模式"; - - // 存储空间设置 - mergedSettings["server.webServer.storage.label"] = "存储空间"; - mergedSettings["server.webServer.storage.cleanUnusedImages"] = "清理未使用的镜像"; - mergedSettings["server.webServer.storage.cleanUnusedVolumes"] = "清理未使用的卷"; - mergedSettings["server.webServer.storage.cleanStoppedContainers"] = "清理已停止的容器"; - mergedSettings["server.webServer.storage.cleanDockerBuilder"] = "清理 Docker Builder 和系统"; - mergedSettings["server.webServer.storage.cleanMonitoring"] = "清理监控数据"; - mergedSettings["server.webServer.storage.cleanAll"] = "清理所有内容"; - - // 个人资料设置 - mergedSettings["profile.title"] = "账户"; - mergedSettings["profile.description"] = "在此更改您的个人资料详情。"; - mergedSettings["profile.email"] = "邮箱"; - mergedSettings["profile.password"] = "密码"; - mergedSettings["profile.avatar"] = "头像"; - - // 外观设置 - mergedSettings["appearance.title"] = "外观"; - mergedSettings["appearance.description"] = "自定义您的仪表盘主题。"; - mergedSettings["appearance.theme"] = "主题"; - mergedSettings["appearance.themeDescription"] = "为您的仪表盘选择主题"; - mergedSettings["appearance.themes.light"] = "明亮"; - mergedSettings["appearance.themes.dark"] = "暗黑"; - mergedSettings["appearance.themes.system"] = "跟随系统"; - mergedSettings["appearance.language"] = "语言"; - mergedSettings["appearance.languageDescription"] = "为您的仪表盘选择语言"; - - // 终端设置 - mergedSettings["terminal.connectionSettings"] = "连接设置"; - mergedSettings["terminal.ipAddress"] = "IP 地址"; - mergedSettings["terminal.port"] = "端口"; - mergedSettings["terminal.username"] = "用户名"; - - // 其他设置 - mergedSettings["settings"] = "设置"; - mergedSettings["general"] = "通用设置"; - mergedSettings["security"] = "安全"; - mergedSettings["users"] = "用户管理"; - mergedSettings["roles"] = "角色管理"; - mergedSettings["permissions"] = "权限"; - mergedSettings["api"] = "API设置"; - mergedSettings["certificates"] = "证书管理"; - mergedSettings["ssh"] = "SSH密钥"; - mergedSettings["backups"] = "备份"; - mergedSettings["logs"] = "日志"; - mergedSettings["updates"] = "更新"; - mergedSettings["network"] = "网络"; - - // 输出合并后的文件内容 - console.log('Common translations total:', Object.keys(mergedCommon).length); - console.log('Settings translations total:', Object.keys(mergedSettings).length); - - // 保存为最终的翻译文件 - fs.writeFileSync(path.join(basePath, 'final-zh-Hans-common.json'), JSON.stringify(mergedCommon, null, 2)); - fs.writeFileSync(path.join(basePath, 'final-zh-Hans-settings.json'), JSON.stringify(mergedSettings, null, 2)); - - // 输出翻译完成的统计 - const commonKeys = Object.keys(mergedCommon); - const settingsKeys = Object.keys(mergedSettings); - console.log('最终翻译文件已保存:'); - console.log(`- 通用翻译 (${commonKeys.length} 个词条)`); - console.log(`- 设置翻译 (${settingsKeys.length} 个词条)`); - - // 创建最终放入项目中的文件(按项目结构) - const projectCommonPath = path.join(basePath, 'apps', 'dokploy', 'public', 'locales', 'zh-Hans'); - - // 确保目录存在 - if (!fs.existsSync(projectCommonPath)) { - fs.mkdirSync(projectCommonPath, { recursive: true }); - console.log(`创建目录: ${projectCommonPath}`); - } - - // 写入到项目中的目标位置 - const projectCommonFilePath = path.join(projectCommonPath, 'common.json'); - const projectSettingsFilePath = path.join(projectCommonPath, 'settings.json'); - - console.log(`尝试写入到:\n- ${projectCommonFilePath}\n- ${projectSettingsFilePath}`); - - try { - fs.writeFileSync(projectCommonFilePath, JSON.stringify(mergedCommon, null, 2)); - fs.writeFileSync(projectSettingsFilePath, JSON.stringify(mergedSettings, null, 2)); - console.log('已成功写入到项目文件夹中!'); - } catch (error) { - console.error('写入到项目文件夹失败:', error.message); - console.log('请手动将文件复制到目标位置。'); - } -} catch (error) { - console.error('错误:', error); -} From cb20950dd9222dba19e3f48404416e4414f7eabb Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 5 Apr 2025 23:03:57 -0600 Subject: [PATCH 19/53] feat(registry): refactor Docker login command execution to use execFileAsync for improved input handling --- apps/dokploy/server/api/routers/registry.ts | 19 +++++++-- .../server/src/utils/process/execAsync.ts | 41 ++++++++++++++++++- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/apps/dokploy/server/api/routers/registry.ts b/apps/dokploy/server/api/routers/registry.ts index a9a6be891..5486f37cd 100644 --- a/apps/dokploy/server/api/routers/registry.ts +++ b/apps/dokploy/server/api/routers/registry.ts @@ -10,8 +10,8 @@ import { import { IS_CLOUD, createRegistry, - execAsync, execAsyncRemote, + execFileAsync, findRegistryById, removeRegistry, updateRegistry, @@ -83,7 +83,13 @@ export const registryRouter = createTRPCRouter({ .input(apiTestRegistry) .mutation(async ({ input }) => { try { - const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`; + const args = [ + "login", + input.registryUrl, + "--username", + input.username, + "--password-stdin", + ]; if (IS_CLOUD && !input.serverId) { throw new TRPCError({ @@ -93,9 +99,14 @@ export const registryRouter = createTRPCRouter({ } if (input.serverId && input.serverId !== "none") { - await execAsyncRemote(input.serverId, loginCommand); + await execAsyncRemote( + input.serverId, + `echo ${input.password} | docker ${args.join(" ")}`, + ); } else { - await execAsync(loginCommand); + await execFileAsync("docker", args, { + input: Buffer.from(input.password).toString(), + }); } return true; diff --git a/packages/server/src/utils/process/execAsync.ts b/packages/server/src/utils/process/execAsync.ts index aee1e821a..c3e409078 100644 --- a/packages/server/src/utils/process/execAsync.ts +++ b/packages/server/src/utils/process/execAsync.ts @@ -1,9 +1,48 @@ -import { exec } from "node:child_process"; +import { exec, execFile } from "node:child_process"; import util from "node:util"; import { findServerById } from "@dokploy/server/services/server"; import { Client } from "ssh2"; + export const execAsync = util.promisify(exec); +export const execFileAsync = async ( + command: string, + args: string[], + options: { input?: string } = {}, +): Promise<{ stdout: string; stderr: string }> => { + const child = execFile(command, args); + + if (options.input && child.stdin) { + child.stdin.write(options.input); + child.stdin.end(); + } + + return new Promise((resolve, reject) => { + let stdout = ""; + let stderr = ""; + + child.stdout?.on("data", (data) => { + stdout += data.toString(); + }); + + child.stderr?.on("data", (data) => { + stderr += data.toString(); + }); + + child.on("close", (code) => { + if (code === 0) { + resolve({ stdout, stderr }); + } else { + reject( + new Error(`Command failed with code ${code}. Stderr: ${stderr}`), + ); + } + }); + + child.on("error", reject); + }); +}; + export const execAsyncRemote = async ( serverId: string | null, command: string, From 14bc26e065dc572b839c6cbd44b1a6ed26d80613 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 6 Apr 2025 00:07:41 -0600 Subject: [PATCH 20/53] feat(websocket): enhance WebSocket server with request validation and client instantiation - Added request validation to ensure user authentication before establishing WebSocket connections. - Refactored WebSocket client instantiation to simplify connection management. --- apps/dokploy/server/wss/drawer-logs.ts | 14 +++++++++---- apps/dokploy/utils/api.ts | 27 +++++++------------------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/apps/dokploy/server/wss/drawer-logs.ts b/apps/dokploy/server/wss/drawer-logs.ts index dcdeaad7d..0202ae521 100644 --- a/apps/dokploy/server/wss/drawer-logs.ts +++ b/apps/dokploy/server/wss/drawer-logs.ts @@ -3,6 +3,7 @@ import { applyWSSHandler } from "@trpc/server/adapters/ws"; import { WebSocketServer } from "ws"; import { appRouter } from "../api/root"; import { createTRPCContext } from "../api/trpc"; +import { validateRequest } from "@dokploy/server/lib/auth"; export const setupDrawerLogsWebSocketServer = ( server: http.Server, @@ -32,8 +33,13 @@ export const setupDrawerLogsWebSocketServer = ( } }); - // Return cleanup function - return () => { - wssTerm.close(); - }; + wssTerm.on("connection", async (ws, req) => { + const _url = new URL(req.url || "", `http://${req.headers.host}`); + const { user, session } = await validateRequest(req); + + if (!user || !session) { + ws.close(); + return; + } + }); }; diff --git a/apps/dokploy/utils/api.ts b/apps/dokploy/utils/api.ts index 7c003f481..56197528b 100644 --- a/apps/dokploy/utils/api.ts +++ b/apps/dokploy/utils/api.ts @@ -27,28 +27,15 @@ const getWsUrl = () => { const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const host = window.location.host; - // Use the base URL for all tRPC WebSocket connections return `${protocol}${host}/drawer-logs`; }; -// Singleton WebSocket client instance -let wsClientInstance: ReturnType | null = null; - -const getWsClient = () => { - if (typeof window === "undefined") return null; - - if (!wsClientInstance) { - wsClientInstance = createWSClient({ - url: getWsUrl() || "", - onClose: () => { - // Reset the instance when connection closes so it can be recreated - wsClientInstance = null; - }, - }); - } - - return wsClientInstance; -}; +const wsClient = + typeof window !== "undefined" + ? createWSClient({ + url: getWsUrl() || "", + }) + : null; /** A set of type-safe react-query hooks for your tRPC API. */ export const api = createTRPCNext({ @@ -70,7 +57,7 @@ export const api = createTRPCNext({ splitLink({ condition: (op) => op.type === "subscription", true: wsLink({ - client: getWsClient()!, + client: wsClient!, }), false: splitLink({ condition: (op) => op.input instanceof FormData, From 1605aedd6e55d6efe299b725e31317e2cbc7a916 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 6 Apr 2025 01:41:47 -0600 Subject: [PATCH 21/53] feat(settings): add HTTPS support and update user schema - Introduced a new boolean field 'https' in the user schema to manage HTTPS settings. - Updated the web domain form to include an HTTPS toggle, allowing users to enable or disable HTTPS. - Enhanced validation logic to ensure certificate type is required when HTTPS is enabled. - Modified Traefik configuration to handle HTTPS routing based on user settings. --- .../dashboard/settings/web-domain.tsx | 111 +- apps/dokploy/drizzle/0084_thin_iron_lad.sql | 1 + apps/dokploy/drizzle/meta/0084_snapshot.json | 5369 +++++++++++++++++ apps/dokploy/drizzle/meta/_journal.json | 7 + apps/dokploy/server/api/routers/settings.ts | 1 + packages/server/src/db/schema/user.ts | 3 + .../server/src/utils/traefik/web-server.ts | 56 +- 7 files changed, 5500 insertions(+), 48 deletions(-) create mode 100644 apps/dokploy/drizzle/0084_thin_iron_lad.sql create mode 100644 apps/dokploy/drizzle/meta/0084_snapshot.json diff --git a/apps/dokploy/components/dashboard/settings/web-domain.tsx b/apps/dokploy/components/dashboard/settings/web-domain.tsx index a579df397..d35dae35b 100644 --- a/apps/dokploy/components/dashboard/settings/web-domain.tsx +++ b/apps/dokploy/components/dashboard/settings/web-domain.tsx @@ -9,6 +9,7 @@ import { import { Form, FormControl, + FormDescription, FormField, FormItem, FormLabel, @@ -22,6 +23,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { GlobeIcon } from "lucide-react"; @@ -33,11 +35,19 @@ import { z } from "zod"; const addServerDomain = z .object({ - domain: z.string().min(1, { message: "URL is required" }), + domain: z.string(), letsEncryptEmail: z.string(), + https: z.boolean().optional(), certificateType: z.enum(["letsencrypt", "none", "custom"]), }) .superRefine((data, ctx) => { + if (data.https && !data.certificateType) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["certificateType"], + message: "Required", + }); + } if (data.certificateType === "letsencrypt" && !data.letsEncryptEmail) { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -61,15 +71,18 @@ export const WebDomain = () => { domain: "", certificateType: "none", letsEncryptEmail: "", + https: false, }, resolver: zodResolver(addServerDomain), }); + const https = form.watch("https"); useEffect(() => { if (data) { form.reset({ domain: data?.user?.host || "", certificateType: data?.user?.certificateType, letsEncryptEmail: data?.user?.letsEncryptEmail || "", + https: data?.user?.https || false, }); } }, [form, form.reset, data]); @@ -79,6 +92,7 @@ export const WebDomain = () => { host: data.domain, letsEncryptEmail: data.letsEncryptEmail, certificateType: data.certificateType, + https: data.https, }) .then(async () => { await refetch(); @@ -155,44 +169,67 @@ export const WebDomain = () => { /> { - return ( - - - {t("settings.server.domain.form.certificate.label")} - - + name="https" + render={({ field }) => ( + +
+ HTTPS + + Automatically provision SSL Certificate. + - - ); - }} +
+ + + +
+ )} /> + {https && ( + { + return ( + + + {t("settings.server.domain.form.certificate.label")} + + + + + ); + }} + /> + )}
- - - - -
- -
- ); -}; - -export default Page; - -Page.getLayout = (page: ReactElement) => { - return {page}; -}; -export async function getServerSideProps( - ctx: GetServerSidePropsContext<{ serviceId: string }>, -) { - const { req, res } = ctx; - const { user, session } = await validateRequest(ctx.req); - if (!user) { - return { - redirect: { - permanent: true, - destination: "/", - }, - }; - } - if (user.role === "member") { - return { - redirect: { - permanent: true, - destination: "/dashboard/settings/profile", - }, - }; - } - - const helpers = createServerSideHelpers({ - router: appRouter, - ctx: { - req: req as any, - res: res as any, - db: null as any, - session: session as any, - user: user as any, - }, - transformer: superjson, - }); - await helpers.user.get.prefetch(); - - return { - props: { - trpcState: helpers.dehydrate(), - }, - }; -} From 8f0697b0e974048baade3f1d114b0d27490d4406 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 6 Apr 2025 15:29:13 -0600 Subject: [PATCH 28/53] Update package.json --- apps/dokploy/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 34ca86faf..be1143bef 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.21.3", + "version": "v0.21.4", "private": true, "license": "Apache-2.0", "type": "module", From 05f43ad06b4c7ccdb95b241f2dabe3636335e6b8 Mon Sep 17 00:00:00 2001 From: Axodouble Date: Mon, 7 Apr 2025 10:36:37 +0200 Subject: [PATCH 29/53] FEAT: Add Dutch / NL language translations --- apps/dokploy/public/locales/nl/common.json | 1 + apps/dokploy/public/locales/nl/settings.json | 58 ++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 apps/dokploy/public/locales/nl/common.json create mode 100644 apps/dokploy/public/locales/nl/settings.json diff --git a/apps/dokploy/public/locales/nl/common.json b/apps/dokploy/public/locales/nl/common.json new file mode 100644 index 000000000..69a88e3b6 --- /dev/null +++ b/apps/dokploy/public/locales/nl/common.json @@ -0,0 +1 @@ +{} diff --git a/apps/dokploy/public/locales/nl/settings.json b/apps/dokploy/public/locales/nl/settings.json new file mode 100644 index 000000000..34c492ec8 --- /dev/null +++ b/apps/dokploy/public/locales/nl/settings.json @@ -0,0 +1,58 @@ +{ + "settings.common.save": "Opslaan", + "settings.common.enterTerminal": "Terminal", + "settings.server.domain.title": "Server Domein", + "settings.server.domain.description": "Voeg een domein toe aan jouw server applicatie.", + "settings.server.domain.form.domain": "Domein", + "settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Email", + "settings.server.domain.form.certificate.label": "Certificaat Aanbieder", + "settings.server.domain.form.certificate.placeholder": "Select een certificaat", + "settings.server.domain.form.certificateOptions.none": "Geen", + "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt", + + "settings.server.webServer.title": "Web Server", + "settings.server.webServer.description": "Herlaad of maak de web server schoon.", + "settings.server.webServer.actions": "Acties", + "settings.server.webServer.reload": "Herladen", + "settings.server.webServer.watchLogs": "Bekijk Logs", + "settings.server.webServer.updateServerIp": "Update de Server IP", + "settings.server.webServer.server.label": "Server", + "settings.server.webServer.traefik.label": "Traefik", + "settings.server.webServer.traefik.modifyEnv": "Bewerk Omgeving", + "settings.server.webServer.traefik.managePorts": "Extra Poort Mappings", + "settings.server.webServer.traefik.managePortsDescription": "Bewerk extra Poorten voor Traefik", + "settings.server.webServer.traefik.targetPort": "Doel Poort", + "settings.server.webServer.traefik.publishedPort": "Gepubliceerde Poort", + "settings.server.webServer.traefik.addPort": "Voeg Poort toe", + "settings.server.webServer.traefik.portsUpdated": "Poorten succesvol aangepast", + "settings.server.webServer.traefik.portsUpdateError": "Poorten niet succesvol aangepast", + "settings.server.webServer.traefik.publishMode": "Publiceer Mode", + "settings.server.webServer.storage.label": "Opslag", + "settings.server.webServer.storage.cleanUnusedImages": "Maak ongebruikte images schoon", + "settings.server.webServer.storage.cleanUnusedVolumes": "Maak ongebruikte volumes schoon", + "settings.server.webServer.storage.cleanStoppedContainers": "Maak gestopte containers schoon", + "settings.server.webServer.storage.cleanDockerBuilder": "Maak Docker Builder & Systeem schoon", + "settings.server.webServer.storage.cleanMonitoring": "Maak monitoor schoon", + "settings.server.webServer.storage.cleanAll": "Maak alles schoon", + + "settings.profile.title": "Account", + "settings.profile.description": "Veramder details van account.", + "settings.profile.email": "Email", + "settings.profile.password": "Wachtwoord", + "settings.profile.avatar": "Profiel Icoon", + + "settings.appearance.title": "Uiterlijk", + "settings.appearance.description": "Verander het thema van je dashboard.", + "settings.appearance.theme": "Thema", + "settings.appearance.themeDescription": "Selecteer een thema voor je dashboard.", + "settings.appearance.themes.light": "Licht", + "settings.appearance.themes.dark": "Donker", + "settings.appearance.themes.system": "Systeem", + "settings.appearance.language": "Taal", + "settings.appearance.languageDescription": "Selecteer een taal voor je dashboard.", + + "settings.terminal.connectionSettings": "Verbindings instellingen", + "settings.terminal.ipAddress": "IP Address", + "settings.terminal.port": "Poort", + "settings.terminal.username": "Gebruikersnaam" +} From 0e1f0b42eeb07a8aa9dafff9b4af16ed27de0287 Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Mon, 7 Apr 2025 21:43:56 +0200 Subject: [PATCH 30/53] fix(gitlab): update group name label and enhance group name handling - Updated the label for the group name input field to indicate it accepts a comma-separated list. - Modified the logic for checking group name inclusion to support multiple names separated by commas. --- .../dashboard/settings/git/gitlab/add-gitlab-provider.tsx | 4 +++- .../dashboard/settings/git/gitlab/edit-gitlab-provider.tsx | 4 +++- packages/server/src/utils/providers/gitlab.ts | 6 +++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx b/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx index 4dd7da93c..023e46ed2 100644 --- a/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx +++ b/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx @@ -248,7 +248,9 @@ export const AddGitlabProvider = () => { name="groupName" render={({ field }) => ( - Group Name (Optional) + + Group Name (Optional, Comma-Separated List) + { name="groupName" render={({ field }) => ( - Group Name (Optional) + + Group Name (Optional, Comma-Separated List) + { const groupName = gitlabProvider.groupName?.toLowerCase(); if (groupName) { - return full_path.toLowerCase().includes(groupName) && kind === "group"; + const isIncluded = groupName + .split(",") + .some((name) => full_path.toLowerCase().includes(name)); + + return isIncluded && kind === "group"; } return kind === "user"; }); From 1279fac1375b043c2812c36748b1883661f81da0 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 19:46:14 +0000 Subject: [PATCH 31/53] [autofix.ci] apply automated fixes --- apps/dokploy/pages/dashboard/project/[projectId].tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/pages/dashboard/project/[projectId].tsx b/apps/dokploy/pages/dashboard/project/[projectId].tsx index 6c4ac4bcc..728d83d11 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId].tsx @@ -365,7 +365,9 @@ const Project = ( switch (service.type) { case "application": - await applicationActions.start.mutateAsync({ applicationId: serviceId }); + await applicationActions.start.mutateAsync({ + applicationId: serviceId, + }); break; case "compose": await composeActions.start.mutateAsync({ composeId: serviceId }); @@ -410,7 +412,9 @@ const Project = ( switch (service.type) { case "application": - await applicationActions.stop.mutateAsync({ applicationId: serviceId }); + await applicationActions.stop.mutateAsync({ + applicationId: serviceId, + }); break; case "compose": await composeActions.stop.mutateAsync({ composeId: serviceId }); From fa698d173ef0f49d54fa812f75659d7016aa7e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20T=C3=B8n=20L=C3=B8vhaug?= Date: Tue, 8 Apr 2025 22:24:19 +0200 Subject: [PATCH 32/53] Move passHostHeader to correct position --- packages/server/src/utils/traefik/web-server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/utils/traefik/web-server.ts b/packages/server/src/utils/traefik/web-server.ts index 2a9971472..1534e2f1c 100644 --- a/packages/server/src/utils/traefik/web-server.ts +++ b/packages/server/src/utils/traefik/web-server.ts @@ -37,9 +37,9 @@ export const updateServerTraefik = ( servers: [ { url: `http://dokploy:${process.env.PORT || 3000}`, - passHostHeader: true, }, ], + passHostHeader: true, }, }, }; From 3ede89fe8adc42ebd0a74fd1d8bc539438102a59 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 20:27:50 +0000 Subject: [PATCH 33/53] [autofix.ci] apply automated fixes --- apps/dokploy/pages/dashboard/project/[projectId].tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/pages/dashboard/project/[projectId].tsx b/apps/dokploy/pages/dashboard/project/[projectId].tsx index 6c4ac4bcc..728d83d11 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId].tsx @@ -365,7 +365,9 @@ const Project = ( switch (service.type) { case "application": - await applicationActions.start.mutateAsync({ applicationId: serviceId }); + await applicationActions.start.mutateAsync({ + applicationId: serviceId, + }); break; case "compose": await composeActions.start.mutateAsync({ composeId: serviceId }); @@ -410,7 +412,9 @@ const Project = ( switch (service.type) { case "application": - await applicationActions.stop.mutateAsync({ applicationId: serviceId }); + await applicationActions.stop.mutateAsync({ + applicationId: serviceId, + }); break; case "compose": await composeActions.stop.mutateAsync({ composeId: serviceId }); From ee6ad07c0a8efe58afa7fab5f57de167bc3b3b59 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Tue, 8 Apr 2025 22:44:17 -0600 Subject: [PATCH 34/53] Update package.json --- apps/dokploy/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index be1143bef..df9f6d53a 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.21.4", + "version": "v0.21.5", "private": true, "license": "Apache-2.0", "type": "module", From 7a5a3de43dd6f6f437406b2c65aa8cc54748a4f3 Mon Sep 17 00:00:00 2001 From: vytenisstaugaitis <30520456+vytenisstaugaitis@users.noreply.github.com> Date: Wed, 9 Apr 2025 18:47:34 +0300 Subject: [PATCH 35/53] fix: correct message on preview deployments disabling --- .../application/preview-deployments/show-preview-settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx index bfc6ad2e4..7bf2063b9 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx @@ -298,7 +298,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => { }) .then(() => { refetch(); - toast.success("Preview deployments enabled"); + toast.success(checked ? "Preview deployments enabled" : "Preview deployments disabled"); }) .catch((error) => { toast.error(error.message); From d335a9515d0c09b8ccc71208472cf82c347d6e04 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:53:37 +0000 Subject: [PATCH 36/53] [autofix.ci] apply automated fixes --- .../preview-deployments/show-preview-settings.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx index 7bf2063b9..4c5068eee 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx @@ -298,7 +298,11 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => { }) .then(() => { refetch(); - toast.success(checked ? "Preview deployments enabled" : "Preview deployments disabled"); + toast.success( + checked + ? "Preview deployments enabled" + : "Preview deployments disabled", + ); }) .catch((error) => { toast.error(error.message); From 37f9e073f084161a15b58289f37d0e43de6c749d Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 12 Apr 2025 02:16:39 -0600 Subject: [PATCH 37/53] fix(railpack): update environment variable handling to include quotes for consistency --- packages/server/src/utils/builders/railpack.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/server/src/utils/builders/railpack.ts b/packages/server/src/utils/builders/railpack.ts index 612e02cf6..55fd40496 100644 --- a/packages/server/src/utils/builders/railpack.ts +++ b/packages/server/src/utils/builders/railpack.ts @@ -84,7 +84,7 @@ export const buildRailpack = async ( for (const envVar of envVariables) { const [key, value] = envVar.split("="); if (key && value) { - buildArgs.push("--secret", `id=${key},env=${key}`); + buildArgs.push("--secret", `id=${key},env='${key}'`); env[key] = value; } } @@ -132,7 +132,7 @@ export const getRailpackCommand = ( ]; for (const env of envVariables) { - prepareArgs.push("--env", env); + prepareArgs.push("--env", `'${env}'`); } // Calculate secrets hash for layer invalidation @@ -164,7 +164,7 @@ export const getRailpackCommand = ( for (const envVar of envVariables) { const [key, value] = envVar.split("="); if (key && value) { - buildArgs.push("--secret", `id=${key},env=${key}`); + buildArgs.push("--secret", `id=${key},env='${key}'`); exportEnvs.push(`export ${key}=${value}`); } } From 773a610be17e96b4446a2ff8b4fc8cd3242c8627 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 12 Apr 2025 02:27:43 -0600 Subject: [PATCH 38/53] fix(profile-form): disable refetch on window focus for user query --- .../settings/profile/profile-form.tsx | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx index 32179378a..7a59469f4 100644 --- a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx @@ -56,6 +56,8 @@ const randomImages = [ export const ProfileForm = () => { const _utils = api.useUtils(); const { data, refetch, isLoading } = api.user.get.useQuery(); + + console.log(data); const { mutateAsync, isLoading: isUpdating, @@ -84,12 +86,17 @@ export const ProfileForm = () => { useEffect(() => { if (data) { - form.reset({ - email: data?.user?.email || "", - password: "", - image: data?.user?.image || "", - currentPassword: "", - }); + form.reset( + { + email: data?.user?.email || "", + password: form.getValues("password") || "", + image: data?.user?.image || "", + currentPassword: form.getValues("currentPassword") || "", + }, + { + keepValues: true, + }, + ); if (data.user.email) { generateSHA256Hash(data.user.email).then((hash) => { @@ -97,8 +104,7 @@ export const ProfileForm = () => { }); } } - form.reset(); - }, [form, form.reset, data]); + }, [form, data]); const onSubmit = async (values: Profile) => { await mutateAsync({ @@ -110,7 +116,12 @@ export const ProfileForm = () => { .then(async () => { await refetch(); toast.success("Profile Updated"); - form.reset(); + form.reset({ + email: values.email, + password: "", + image: values.image, + currentPassword: "", + }); }) .catch(() => { toast.error("Error updating the profile"); From efee79888039298a84dba39e3fcb03f868bb4c4b Mon Sep 17 00:00:00 2001 From: Ron_Tayler Date: Sat, 12 Apr 2025 22:02:27 +0300 Subject: [PATCH 39/53] Fixed network search in Traefik Labels for the service --- packages/server/src/utils/docker/domain.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/server/src/utils/docker/domain.ts b/packages/server/src/utils/docker/domain.ts index 5a68146a6..4f0083979 100644 --- a/packages/server/src/utils/docker/domain.ts +++ b/packages/server/src/utils/docker/domain.ts @@ -249,6 +249,11 @@ export const addDomainToCompose = async ( labels.unshift("traefik.enable=true"); } labels.unshift(...httpLabels); + if (!compose.isolatedDeployment) { + if (!labels.includes("traefik.docker.network=dokploy-network")) { + labels.unshift("traefik.docker.network=dokploy-network"); + } + } } if (!compose.isolatedDeployment) { From 9b5cd0f5fe043dabb8d36558702c0b630711d1e6 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 12 Apr 2025 21:11:21 -0600 Subject: [PATCH 40/53] chore: update dependencies and enhance 2FA form - Updated `better-auth` to version 1.2.6 in multiple package.json files. - Updated `@better-auth/utils` to version 0.2.4 in server package.json. - Added optional `issuer` field to the 2FA form for enhanced user experience. - Removed unnecessary console log from the profile form component. --- .../dashboard/settings/profile/enable-2fa.tsx | 22 +++++ .../settings/profile/profile-form.tsx | 1 - apps/dokploy/package.json | 2 +- packages/server/package.json | 4 +- pnpm-lock.yaml | 88 ++++++++----------- 5 files changed, 62 insertions(+), 55 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx b/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx index 6cf2c6a53..1cfa7574e 100644 --- a/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx @@ -36,6 +36,7 @@ const PasswordSchema = z.object({ password: z.string().min(8, { message: "Password is required", }), + issuer: z.string().optional(), }); const PinSchema = z.object({ @@ -66,6 +67,7 @@ export const Enable2FA = () => { try { const { data: enableData, error } = await authClient.twoFactor.enable({ password: formData.password, + issuer: formData.issuer, }); if (!enableData) { @@ -217,6 +219,26 @@ export const Enable2FA = () => { )} /> + ( + + Issuer + + + + + Enter your password to enable 2FA + + + + )} + /> From 8e8bc3e71e3c836dce085bc370be933b47bf570f Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 17 Apr 2025 01:58:25 -0600 Subject: [PATCH 49/53] Enhance PostgreSQL backup command in web server utility - Added error handling to check for the existence of the PostgreSQL container before executing the backup command. - Updated the backup command to use the retrieved container ID, ensuring the command runs correctly. --- packages/server/src/utils/backups/web-server.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/server/src/utils/backups/web-server.ts b/packages/server/src/utils/backups/web-server.ts index a7d48a2fa..ef2249d0a 100644 --- a/packages/server/src/utils/backups/web-server.ts +++ b/packages/server/src/utils/backups/web-server.ts @@ -23,7 +23,17 @@ export const runWebServerBackup = async (backup: BackupSchedule) => { try { await execAsync(`mkdir -p ${tempDir}/filesystem`); - const postgresCommand = `docker exec $(docker ps --filter "name=dokploy-postgres" -q) pg_dump -v -Fc -U dokploy -d dokploy > ${tempDir}/database.sql`; + // First get the container ID + const { stdout: containerId } = await execAsync( + "docker ps --filter 'name=dokploy-postgres' -q", + ); + + if (!containerId) { + throw new Error("PostgreSQL container not found"); + } + + // Then run pg_dump with the container ID + const postgresCommand = `docker exec ${containerId.trim()} pg_dump -v -Fc -U dokploy -d dokploy > '${tempDir}/database.sql'`; await execAsync(postgresCommand); await execAsync(`cp -r ${BASE_PATH}/* ${tempDir}/filesystem/`); From 33ab87f3db3dea19362ba1f2bc9e6fa1d8288c97 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 17 Apr 2025 02:20:03 -0600 Subject: [PATCH 50/53] fix(gitlab): enhance group name matching logic to support multiple names - Updated the group name check to allow for a comma-separated list of names, improving flexibility in group name validation. --- packages/server/src/utils/providers/gitlab.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/server/src/utils/providers/gitlab.ts b/packages/server/src/utils/providers/gitlab.ts index b8a58fd82..d01cc4004 100644 --- a/packages/server/src/utils/providers/gitlab.ts +++ b/packages/server/src/utils/providers/gitlab.ts @@ -435,7 +435,9 @@ export const testGitlabConnection = async ( const { full_path, kind } = repo.namespace; if (groupName) { - return full_path.toLowerCase().includes(groupName) && kind === "group"; + return groupName + .split(",") + .some((name) => full_path.toLowerCase().includes(name)); } return kind === "user"; }); From 43a17e7e75223f6f2c98b1bff3b4b077faadd746 Mon Sep 17 00:00:00 2001 From: Khiet Tam Nguyen Date: Fri, 18 Apr 2025 12:49:02 +1000 Subject: [PATCH 51/53] style: remove double space --- .../components/dashboard/application/general/generic/show.tsx | 2 +- apps/dokploy/components/dashboard/settings/ai-form.tsx | 2 +- .../dashboard/settings/certificates/show-certificates.tsx | 2 +- .../dashboard/settings/cluster/registry/show-registry.tsx | 2 +- .../dashboard/settings/destination/show-destinations.tsx | 2 +- .../dashboard/settings/notifications/show-notifications.tsx | 2 +- .../components/dashboard/settings/ssh-keys/show-ssh-keys.tsx | 2 +- .../[projectId]/services/application/[applicationId].tsx | 2 +- .../project/[projectId]/services/compose/[composeId].tsx | 2 +- .../project/[projectId]/services/mariadb/[mariadbId].tsx | 2 +- .../dashboard/project/[projectId]/services/mongo/[mongoId].tsx | 2 +- .../dashboard/project/[projectId]/services/mysql/[mysqlId].tsx | 2 +- .../project/[projectId]/services/postgres/[postgresId].tsx | 2 +- .../dashboard/project/[projectId]/services/redis/[redisId].tsx | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/general/generic/show.tsx b/apps/dokploy/components/dashboard/application/general/generic/show.tsx index 3f8854888..9b9a0ba05 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/show.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/show.tsx @@ -65,7 +65,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => { setSab(e as TabState); }} > -
+
{ key={config.aiId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
{config.name} diff --git a/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx b/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx index 6aaa25630..b80c7b549 100644 --- a/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx +++ b/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx @@ -70,7 +70,7 @@ export const ShowCertificates = () => { key={certificate.certificateId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
diff --git a/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx b/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx index 08cb03813..9ae595d6f 100644 --- a/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx +++ b/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx @@ -54,7 +54,7 @@ export const ShowRegistry = () => { key={registry.registryId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
diff --git a/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx b/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx index 0639b0f75..014596ce3 100644 --- a/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx +++ b/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx @@ -55,7 +55,7 @@ export const ShowDestinations = () => { key={destination.destinationId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
{index + 1}. {destination.name} diff --git a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx index 782b92413..26ac17932 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx @@ -61,7 +61,7 @@ export const ShowNotifications = () => { key={notification.notificationId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
{notification.notificationType === "slack" && (
diff --git a/apps/dokploy/components/dashboard/settings/ssh-keys/show-ssh-keys.tsx b/apps/dokploy/components/dashboard/settings/ssh-keys/show-ssh-keys.tsx index 5842457ba..00d685a8d 100644 --- a/apps/dokploy/components/dashboard/settings/ssh-keys/show-ssh-keys.tsx +++ b/apps/dokploy/components/dashboard/settings/ssh-keys/show-ssh-keys.tsx @@ -56,7 +56,7 @@ export const ShowDestinations = () => { key={sshKey.sshKeyId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx index cff3a8db2..bf5ced4a9 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx @@ -215,7 +215,7 @@ const Service = ( router.push(newPath); }} > -
+
-
+
-
+
-
+
-
+
-
+
-
+
Date: Fri, 18 Apr 2025 12:54:42 +1000 Subject: [PATCH 52/53] fix: add overflow-x-scroll to tab list container --- .../[projectId]/services/application/[applicationId].tsx | 2 +- .../project/[projectId]/services/compose/[composeId].tsx | 2 +- .../project/[projectId]/services/mariadb/[mariadbId].tsx | 2 +- .../dashboard/project/[projectId]/services/mongo/[mongoId].tsx | 2 +- .../dashboard/project/[projectId]/services/mysql/[mysqlId].tsx | 2 +- .../project/[projectId]/services/postgres/[postgresId].tsx | 2 +- .../dashboard/project/[projectId]/services/redis/[redisId].tsx | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx index bf5ced4a9..91af2cfab 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx @@ -215,7 +215,7 @@ const Service = ( router.push(newPath); }} > -
+
-
+
-
+
-
+
-
+
-
+
-
+
Date: Fri, 18 Apr 2025 13:01:43 +1000 Subject: [PATCH 53/53] fix: grid cols start from lg instead of md for compose --- .../project/[projectId]/services/compose/[composeId].tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx index 91da1623c..3bba9eb27 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx @@ -215,12 +215,12 @@ const Service = (
General