From 611b0b3113155ff05457aefa99a008ebbe88f6d8 Mon Sep 17 00:00:00 2001 From: Illia Shchukin Date: Mon, 5 Jan 2026 13:55:52 +0200 Subject: [PATCH 01/40] fix: allow users to open projects with accessible environments - Update environment selection to fallback to first accessible environment when default is not accessible - Fix search command to handle users without default environment access - Fix projects list to use accessible environment instead of always default - Add server-side redirect to accessible environment when accessing inaccessible one - Add comprehensive test coverage for environment access fallback logic Fixes #3394 --- .../env/environment-access-fallback.test.ts | 295 ++++++++++++++++++ .../components/dashboard/projects/show.tsx | 10 +- .../components/dashboard/search-command.tsx | 2 +- .../environment/[environmentId].tsx | 35 ++- 4 files changed, 334 insertions(+), 8 deletions(-) create mode 100644 apps/dokploy/__test__/env/environment-access-fallback.test.ts diff --git a/apps/dokploy/__test__/env/environment-access-fallback.test.ts b/apps/dokploy/__test__/env/environment-access-fallback.test.ts new file mode 100644 index 000000000..1a19dd813 --- /dev/null +++ b/apps/dokploy/__test__/env/environment-access-fallback.test.ts @@ -0,0 +1,295 @@ +import { describe, expect, it } from "vitest"; + +// Type definitions matching the project structure +type Environment = { + environmentId: string; + name: string; + isDefault: boolean; +}; + +type Project = { + projectId: string; + name: string; + environments: Environment[]; +}; + +/** + * Helper function that selects the appropriate environment for a user + * This matches the logic used in search-command.tsx and show.tsx + */ +function selectAccessibleEnvironment( + project: Project | null | undefined, +): Environment | null { + if (!project || !project.environments || project.environments.length === 0) { + return null; + } + + // Find default environment from accessible environments, or fall back to first accessible environment + const defaultEnvironment = + project.environments.find((environment) => environment.isDefault) || + project.environments[0]; + + return defaultEnvironment || null; +} + +describe("Environment Access Fallback", () => { + describe("selectAccessibleEnvironment", () => { + it("should return default environment when user has access to it", () => { + const project: Project = { + projectId: "proj-1", + name: "Test Project", + environments: [ + { + environmentId: "env-prod", + name: "production", + isDefault: true, + }, + { + environmentId: "env-dev", + name: "development", + isDefault: false, + }, + ], + }; + + const result = selectAccessibleEnvironment(project); + + expect(result).not.toBeNull(); + expect(result?.environmentId).toBe("env-prod"); + expect(result?.isDefault).toBe(true); + }); + + it("should return first accessible environment when user doesn't have access to default", () => { + // Simulating filtered environments (user only has access to development) + const project: Project = { + projectId: "proj-1", + name: "Test Project", + environments: [ + // Note: production is not in the list because user doesn't have access + { + environmentId: "env-dev", + name: "development", + isDefault: false, + }, + { + environmentId: "env-staging", + name: "staging", + isDefault: false, + }, + ], + }; + + const result = selectAccessibleEnvironment(project); + + expect(result).not.toBeNull(); + expect(result?.environmentId).toBe("env-dev"); + expect(result?.name).toBe("development"); + }); + + it("should return first environment when no default is marked but environments exist", () => { + const project: Project = { + projectId: "proj-1", + name: "Test Project", + environments: [ + { + environmentId: "env-dev", + name: "development", + isDefault: false, + }, + { + environmentId: "env-staging", + name: "staging", + isDefault: false, + }, + ], + }; + + const result = selectAccessibleEnvironment(project); + + expect(result).not.toBeNull(); + expect(result?.environmentId).toBe("env-dev"); + }); + + it("should return null when project has no accessible environments", () => { + const project: Project = { + projectId: "proj-1", + name: "Test Project", + environments: [], + }; + + const result = selectAccessibleEnvironment(project); + + expect(result).toBeNull(); + }); + + it("should return null when project is null", () => { + const result = selectAccessibleEnvironment(null); + + expect(result).toBeNull(); + }); + + it("should return null when project is undefined", () => { + const result = selectAccessibleEnvironment(undefined); + + expect(result).toBeNull(); + }); + + it("should handle project with single accessible environment", () => { + const project: Project = { + projectId: "proj-1", + name: "Test Project", + environments: [ + { + environmentId: "env-dev", + name: "development", + isDefault: false, + }, + ], + }; + + const result = selectAccessibleEnvironment(project); + + expect(result).not.toBeNull(); + expect(result?.environmentId).toBe("env-dev"); + }); + + it("should prioritize default environment even when it's not first in the array", () => { + const project: Project = { + projectId: "proj-1", + name: "Test Project", + environments: [ + { + environmentId: "env-dev", + name: "development", + isDefault: false, + }, + { + environmentId: "env-staging", + name: "staging", + isDefault: false, + }, + { + environmentId: "env-prod", + name: "production", + isDefault: true, + }, + ], + }; + + const result = selectAccessibleEnvironment(project); + + expect(result).not.toBeNull(); + expect(result?.environmentId).toBe("env-prod"); + expect(result?.isDefault).toBe(true); + }); + + it("should handle multiple default environments by returning the first one found", () => { + // Edge case: multiple environments marked as default (shouldn't happen, but test it) + const project: Project = { + projectId: "proj-1", + name: "Test Project", + environments: [ + { + environmentId: "env-prod-1", + name: "production-1", + isDefault: true, + }, + { + environmentId: "env-prod-2", + name: "production-2", + isDefault: true, + }, + ], + }; + + const result = selectAccessibleEnvironment(project); + + expect(result).not.toBeNull(); + expect(result?.isDefault).toBe(true); + // Should return the first default found + expect(result?.environmentId).toBe("env-prod-1"); + }); + + it("should work correctly when user has access to multiple environments including default", () => { + const project: Project = { + projectId: "proj-1", + name: "Test Project", + environments: [ + { + environmentId: "env-prod", + name: "production", + isDefault: true, + }, + { + environmentId: "env-dev", + name: "development", + isDefault: false, + }, + { + environmentId: "env-staging", + name: "staging", + isDefault: false, + }, + ], + }; + + const result = selectAccessibleEnvironment(project); + + expect(result).not.toBeNull(); + expect(result?.environmentId).toBe("env-prod"); + expect(result?.isDefault).toBe(true); + }); + + it("should handle real-world scenario: user with only development access", () => { + // This simulates the exact bug we're fixing: + // User has access to development but not production (default) + // The filtered environments array only contains development + const project: Project = { + projectId: "proj-1", + name: "My Project", + environments: [ + // Only development is accessible (production was filtered out) + { + environmentId: "env-dev-123", + name: "development", + isDefault: false, + }, + ], + }; + + const result = selectAccessibleEnvironment(project); + + expect(result).not.toBeNull(); + expect(result?.environmentId).toBe("env-dev-123"); + expect(result?.name).toBe("development"); + // Should not be null even though it's not the default + }); + }); + + describe("Environment selection edge cases", () => { + it("should handle project with environments property as undefined", () => { + const project = { + projectId: "proj-1", + name: "Test Project", + environments: undefined, + } as unknown as Project; + + const result = selectAccessibleEnvironment(project); + + expect(result).toBeNull(); + }); + + it("should handle project with null environments array", () => { + const project = { + projectId: "proj-1", + name: "Test Project", + environments: null, + } as unknown as Project; + + const result = selectAccessibleEnvironment(project); + + expect(result).toBeNull(); + }); + }); +}); + diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index a618a20ac..6dcff247a 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -288,9 +288,11 @@ export const ShowProjects = () => { ) .some(Boolean); - const productionEnvironment = project?.environments.find( - (env) => env.isDefault, - ); + // Find default environment from accessible environments, or fall back to first accessible environment + const accessibleEnvironment = + project?.environments.find( + (env) => env.isDefault, + ) || project?.environments?.[0]; return (
{ className="w-full lg:max-w-md" > {haveServicesWithDomains ? ( diff --git a/apps/dokploy/components/dashboard/search-command.tsx b/apps/dokploy/components/dashboard/search-command.tsx index d53fe0037..6fd798955 100644 --- a/apps/dokploy/components/dashboard/search-command.tsx +++ b/apps/dokploy/components/dashboard/search-command.tsx @@ -89,7 +89,7 @@ export const SearchCommand = () => { {data?.map((project) => { - // Find default environment, or fall back to first environment + // Find default environment from accessible environments, or fall back to first accessible environment const defaultEnvironment = project.environments.find( (environment) => environment.isDefault, diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx index dcc34cec2..86ef93c34 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx @@ -1621,9 +1621,38 @@ export async function getServerSideProps( projectId: params.projectId, }); - await helpers.environment.one.fetch({ - environmentId: params.environmentId, - }); + // Try to fetch the requested environment + try { + await helpers.environment.one.fetch({ + environmentId: params.environmentId, + }); + } catch (error) { + // If user doesn't have access to requested environment, redirect to accessible one + const accessibleEnvironments = await helpers.environment.byProjectId.fetch({ + projectId: params.projectId, + }); + + if (accessibleEnvironments.length > 0) { + // Try to find default, otherwise use first accessible + const targetEnv = + accessibleEnvironments.find((env) => env.isDefault) || + accessibleEnvironments[0]; + + return { + redirect: { + permanent: false, + destination: `/dashboard/project/${params.projectId}/environment/${targetEnv.environmentId}`, + }, + }; + } + // No accessible environments, redirect to home + return { + redirect: { + permanent: false, + destination: "/", + }, + }; + } await helpers.environment.byProjectId.fetch({ projectId: params.projectId, From 9e8c3f15250dea4534bb1517fdfc694bbfa02be0 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 16:23:54 +0000 Subject: [PATCH 02/40] [autofix.ci] apply automated fixes --- .../__test__/env/environment-access-fallback.test.ts | 1 - apps/dokploy/components/dashboard/projects/show.tsx | 5 ++--- .../project/[projectId]/environment/[environmentId].tsx | 7 ++++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/dokploy/__test__/env/environment-access-fallback.test.ts b/apps/dokploy/__test__/env/environment-access-fallback.test.ts index 1a19dd813..a4b56393a 100644 --- a/apps/dokploy/__test__/env/environment-access-fallback.test.ts +++ b/apps/dokploy/__test__/env/environment-access-fallback.test.ts @@ -292,4 +292,3 @@ describe("Environment Access Fallback", () => { }); }); }); - diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index 6dcff247a..eb1ff5ab6 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -290,9 +290,8 @@ export const ShowProjects = () => { // Find default environment from accessible environments, or fall back to first accessible environment const accessibleEnvironment = - project?.environments.find( - (env) => env.isDefault, - ) || project?.environments?.[0]; + project?.environments.find((env) => env.isDefault) || + project?.environments?.[0]; return (
0) { // Try to find default, otherwise use first accessible From 5d26df9d9f051af6c3e074cf295fe62f4c9e82cd Mon Sep 17 00:00:00 2001 From: Amir Moradi <1281163+amirhmoradi@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:13:32 +0100 Subject: [PATCH 03/40] Delete apps/dokploy/drizzle/0057_damp_prism.sql This migration file is not used nor present in the journal. This is a legacy file that did not get cleaned. I am removing the file to clean the state of the migrations and allow for custom ci/cd scripts to have a clean run and avoid duplicated migration ids (this file conflicts with the `0057_tricky_living_tribunal...`) --- apps/dokploy/drizzle/0057_damp_prism.sql | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 apps/dokploy/drizzle/0057_damp_prism.sql diff --git a/apps/dokploy/drizzle/0057_damp_prism.sql b/apps/dokploy/drizzle/0057_damp_prism.sql deleted file mode 100644 index 363c2a9f4..000000000 --- a/apps/dokploy/drizzle/0057_damp_prism.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE IF NOT EXISTS "ai" ( - "aiId" text PRIMARY KEY NOT NULL, - "name" text NOT NULL, - "apiUrl" text NOT NULL, - "apiKey" text NOT NULL, - "model" text NOT NULL, - "isEnabled" boolean DEFAULT true NOT NULL, - "adminId" text NOT NULL, - "createdAt" text NOT NULL -); ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "ai" ADD CONSTRAINT "ai_adminId_admin_adminId_fk" FOREIGN KEY ("adminId") REFERENCES "public"."admin"("adminId") ON DELETE cascade ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; From a2df52ea7c3b39672f9cd8ae1ee12e92385c6dd6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 15:32:01 +0000 Subject: [PATCH 04/40] Initial plan From f39b511316a5fc4b7a8b605d568a4f2dd82866b3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 15:39:04 +0000 Subject: [PATCH 05/40] Fix environment variable resolution for Stack compose type Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com> --- .../__test__/env/stack-environment.test.ts | 188 ++++++++++++++++++ packages/server/src/utils/builders/compose.ts | 1 + 2 files changed, 189 insertions(+) create mode 100644 apps/dokploy/__test__/env/stack-environment.test.ts diff --git a/apps/dokploy/__test__/env/stack-environment.test.ts b/apps/dokploy/__test__/env/stack-environment.test.ts new file mode 100644 index 000000000..e4eb8bea0 --- /dev/null +++ b/apps/dokploy/__test__/env/stack-environment.test.ts @@ -0,0 +1,188 @@ +import { getEnviromentVariablesObject } from "@dokploy/server/index"; +import { describe, expect, it } from "vitest"; + +const projectEnv = ` +ENVIRONMENT=staging +DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db +PORT=3000 +`; + +const environmentEnv = ` +NODE_ENV=development +API_URL=https://api.dev.example.com +REDIS_URL=redis://localhost:6379 +DATABASE_NAME=dev_database +SECRET_KEY=env-secret-123 +`; + +describe("getEnviromentVariablesObject with environment variables (Stack compose)", () => { + it("resolves environment variables correctly for Stack compose", () => { + const serviceEnv = ` +FOO=\${{environment.NODE_ENV}} +BAR=\${{environment.API_URL}} +BAZ=test +`; + + const result = getEnviromentVariablesObject( + serviceEnv, + projectEnv, + environmentEnv, + ); + + expect(result).toEqual({ + FOO: "development", + BAR: "https://api.dev.example.com", + BAZ: "test", + }); + }); + + it("resolves both project and environment variables for Stack compose", () => { + const serviceEnv = ` +ENVIRONMENT=\${{project.ENVIRONMENT}} +NODE_ENV=\${{environment.NODE_ENV}} +API_URL=\${{environment.API_URL}} +DATABASE_URL=\${{project.DATABASE_URL}} +SERVICE_PORT=4000 +`; + + const result = getEnviromentVariablesObject( + serviceEnv, + projectEnv, + environmentEnv, + ); + + expect(result).toEqual({ + ENVIRONMENT: "staging", + NODE_ENV: "development", + API_URL: "https://api.dev.example.com", + DATABASE_URL: "postgres://postgres:postgres@localhost:5432/project_db", + SERVICE_PORT: "4000", + }); + }); + + it("handles multiple environment references in single value for Stack compose", () => { + const multiRefEnv = ` +HOST=localhost +PORT=5432 +USERNAME=postgres +PASSWORD=secret123 +`; + + const serviceEnv = ` +DATABASE_URL=postgresql://\${{environment.USERNAME}}:\${{environment.PASSWORD}}@\${{environment.HOST}}:\${{environment.PORT}}/mydb +`; + + const result = getEnviromentVariablesObject( + serviceEnv, + "", + multiRefEnv, + ); + + expect(result).toEqual({ + DATABASE_URL: "postgresql://postgres:secret123@localhost:5432/mydb", + }); + }); + + it("throws error for undefined environment variables in Stack compose", () => { + const serviceWithUndefined = ` +UNDEFINED_VAR=\${{environment.UNDEFINED_VAR}} +`; + + expect(() => + getEnviromentVariablesObject(serviceWithUndefined, "", environmentEnv), + ).toThrow("Invalid environment variable: environment.UNDEFINED_VAR"); + }); + + it("allows service variables to override environment variables in Stack compose", () => { + const serviceOverrideEnv = ` +NODE_ENV=production +API_URL=\${{environment.API_URL}} +`; + + const result = getEnviromentVariablesObject( + serviceOverrideEnv, + "", + environmentEnv, + ); + + expect(result).toEqual({ + NODE_ENV: "production", + API_URL: "https://api.dev.example.com", + }); + }); + + it("resolves complex references with project, environment, and service variables for Stack compose", () => { + const complexServiceEnv = ` +FULL_DATABASE_URL=\${{project.DATABASE_URL}}/\${{environment.DATABASE_NAME}} +API_ENDPOINT=\${{environment.API_URL}}/\${{project.ENVIRONMENT}}/api +SERVICE_NAME=my-service +COMPLEX_VAR=\${{SERVICE_NAME}}-\${{environment.NODE_ENV}}-\${{project.ENVIRONMENT}} +`; + + const result = getEnviromentVariablesObject( + complexServiceEnv, + projectEnv, + environmentEnv, + ); + + expect(result).toEqual({ + FULL_DATABASE_URL: + "postgres://postgres:postgres@localhost:5432/project_db/dev_database", + API_ENDPOINT: "https://api.dev.example.com/staging/api", + SERVICE_NAME: "my-service", + COMPLEX_VAR: "my-service-development-staging", + }); + }); + + it("maintains precedence: service > environment > project in Stack compose", () => { + const conflictingProjectEnv = ` +NODE_ENV=production-project +API_URL=https://project.api.com +DATABASE_NAME=project_db +`; + + const conflictingEnvironmentEnv = ` +NODE_ENV=development-environment +API_URL=https://environment.api.com +DATABASE_NAME=env_db +`; + + const serviceWithConflicts = ` +NODE_ENV=service-override +PROJECT_ENV=\${{project.NODE_ENV}} +ENV_VAR=\${{environment.API_URL}} +DB_NAME=\${{environment.DATABASE_NAME}} +`; + + const result = getEnviromentVariablesObject( + serviceWithConflicts, + conflictingProjectEnv, + conflictingEnvironmentEnv, + ); + + expect(result).toEqual({ + NODE_ENV: "service-override", + PROJECT_ENV: "production-project", + ENV_VAR: "https://environment.api.com", + DB_NAME: "env_db", + }); + }); + + it("handles empty environment variables in Stack compose", () => { + const serviceWithEmpty = ` +SERVICE_VAR=test +PROJECT_VAR=\${{project.ENVIRONMENT}} +`; + + const result = getEnviromentVariablesObject( + serviceWithEmpty, + projectEnv, + "", + ); + + expect(result).toEqual({ + SERVICE_VAR: "test", + PROJECT_VAR: "staging", + }); + }); +}); diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index 5a0184a3d..5eede59d5 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -134,6 +134,7 @@ const getExportEnvCommand = (compose: ComposeNested) => { const envVars = getEnviromentVariablesObject( compose.env, compose.environment.project.env, + compose.environment.env, ); const exports = Object.entries(envVars) .map(([key, value]) => `${key}=${quote([value])}`) From c1d452bcf77215e199a69cf76282e61a0f293a4e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 15:43:01 +0000 Subject: [PATCH 06/40] Complete fix for Stack compose environment variable substitution Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com> --- apps/dokploy/__test__/env/stack-environment.test.ts | 6 +----- .../preview-deployments/show-preview-deployments.tsx | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/dokploy/__test__/env/stack-environment.test.ts b/apps/dokploy/__test__/env/stack-environment.test.ts index e4eb8bea0..13f5adb53 100644 --- a/apps/dokploy/__test__/env/stack-environment.test.ts +++ b/apps/dokploy/__test__/env/stack-environment.test.ts @@ -72,11 +72,7 @@ PASSWORD=secret123 DATABASE_URL=postgresql://\${{environment.USERNAME}}:\${{environment.PASSWORD}}@\${{environment.HOST}}:\${{environment.PORT}}/mydb `; - const result = getEnviromentVariablesObject( - serviceEnv, - "", - multiRefEnv, - ); + const result = getEnviromentVariablesObject(serviceEnv, "", multiRefEnv); expect(result).toEqual({ DATABASE_URL: "postgresql://postgres:secret123@localhost:5432/mydb", diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx index 23127383a..6cf8d8830 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx @@ -1,3 +1,4 @@ +import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import { ExternalLink, FileText, @@ -29,7 +30,6 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import { api } from "@/utils/api"; import { ShowModalLogs } from "../../settings/web-server/show-modal-logs"; import { ShowDeploymentsModal } from "../deployments/show-deployments-modal"; From 7b3f0273cb2ab76c2b0725738014c99bc682e409 Mon Sep 17 00:00:00 2001 From: bdkopen Date: Thu, 15 Jan 2026 21:31:14 -0500 Subject: [PATCH 07/40] chore: uninstall disabled `lefthook` package --- lefthook.yml | 45 ---------------------- package.json | 1 - pnpm-lock.yaml | 100 ------------------------------------------------- 3 files changed, 146 deletions(-) delete mode 100644 lefthook.yml diff --git a/lefthook.yml b/lefthook.yml deleted file mode 100644 index 3f5a6d09f..000000000 --- a/lefthook.yml +++ /dev/null @@ -1,45 +0,0 @@ -# EXAMPLE USAGE: -# -# Refer for explanation to following link: -# https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md -# -# pre-push: -# commands: -# packages-audit: -# tags: frontend security -# run: yarn audit -# gems-audit: -# tags: backend security -# run: bundle audit -# -# pre-commit: -# parallel: true -# commands: -# eslint: -# glob: "*.{js,ts,jsx,tsx}" -# run: yarn eslint {staged_files} -# rubocop: -# tags: backend style -# glob: "*.rb" -# exclude: '(^|/)(application|routes)\.rb$' -# run: bundle exec rubocop --force-exclusion {all_files} -# govet: -# tags: backend style -# files: git ls-files -m -# glob: "*.go" -# run: go vet {files} -# scripts: -# "hello.js": -# runner: node -# "any.go": -# runner: go run - -commit-msg: - commands: - commitlint: - # run: "npx commitlint --edit $1" - -pre-commit: - commands: - check: - # run: "pnpm check" diff --git a/package.json b/package.json index 1f59cc661..7cd56ce15 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "@types/node": "^18.19.104", "dotenv": "16.4.5", "esbuild": "0.20.2", - "lefthook": "1.8.4", "lint-staged": "^15.5.2", "tsx": "4.16.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd5ef0a96..6cc147e8b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,9 +30,6 @@ importers: esbuild: specifier: 0.20.2 version: 0.20.2 - lefthook: - specifier: 1.8.4 - version: 1.8.4 lint-staged: specifier: ^15.5.2 version: 15.5.2 @@ -5781,60 +5778,6 @@ packages: leac@0.6.0: resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} - lefthook-darwin-arm64@1.8.4: - resolution: {integrity: sha512-OS5MsU0gvd8LYSpuQCHtmDUqwNrJ/LjCO0LGC1wNepY4OkuVl9DfX+rQ506CVUQYZiGVcwy2/qPOOBjNzA5+wQ==} - cpu: [arm64] - os: [darwin] - - lefthook-darwin-x64@1.8.4: - resolution: {integrity: sha512-QLRsqK9aTMRcVW8qz4pzI2OWnGCEcaEPJlIiFjwstYsS+wfkooxOS0UkfVMjy+QoGgEcki+cxF/FoY7lE7DDtw==} - cpu: [x64] - os: [darwin] - - lefthook-freebsd-arm64@1.8.4: - resolution: {integrity: sha512-chnQ1m/Cmn9c0sLdk5HL2SToE5LBJv5uQMdH1IGRRcw+nEqWqrMnDXvM75caiJAyjmUGvPH3czKTJDzTFV1E+A==} - cpu: [arm64] - os: [freebsd] - - lefthook-freebsd-x64@1.8.4: - resolution: {integrity: sha512-KQi+WBUdnGLnK0rHOR58kbMH5TDVN1ZjZLu66Pv9FCG7Y7shR1qtaTXu+wmxdRhMvaLeQIXRsUEPjNRC66yMmA==} - cpu: [x64] - os: [freebsd] - - lefthook-linux-arm64@1.8.4: - resolution: {integrity: sha512-CXNcqIskLwTwQARidGdFqmNxpvOU3jsWPK4KA7pq2+QmlWJ64w98ebMvNBoUmRUCXqzmUm7Udf/jpfz2fobewQ==} - cpu: [arm64] - os: [linux] - - lefthook-linux-x64@1.8.4: - resolution: {integrity: sha512-pVNITkFBxUCEtamWSM/res2Gd48+m9YKbNyIBndAuZVC5pKV5aGKZy2DNq6PWUPYiUDPx+7hoAtCJg/tlAiqhw==} - cpu: [x64] - os: [linux] - - lefthook-openbsd-arm64@1.8.4: - resolution: {integrity: sha512-l+i/Dg5X36kYzhpMGSPE3rMbWy1KSytbLB9lY1PmxYb6LRH6iQTYIoxvLabVUwSBPSq8HtIFa50+bvC5+scfVA==} - cpu: [arm64] - os: [openbsd] - - lefthook-openbsd-x64@1.8.4: - resolution: {integrity: sha512-CqhDDPPX8oHzMLgNi/Reba823DRzj+eMNWQ8axvSiIG+zmG1w20xZH5QSs/mD3tjrND90yfDd90mWMt181qPyA==} - cpu: [x64] - os: [openbsd] - - lefthook-windows-arm64@1.8.4: - resolution: {integrity: sha512-dvpvorICmVjmw29Aiczg7DcaSzkd86bEBomiGq4UsAEk3+7ExLrlWJDLFsI6xLjMKmTxy+F7eXb2uDtuFC1N4g==} - cpu: [arm64] - os: [win32] - - lefthook-windows-x64@1.8.4: - resolution: {integrity: sha512-e+y8Jt4/7PnoplhOuK48twjGVJEsU4T3J5kxD4mWfl6Cbit0YSn4bme9nW41eqCqTUqOm+ky29XlfnPHFX5ZNA==} - cpu: [x64] - os: [win32] - - lefthook@1.8.4: - resolution: {integrity: sha512-XNyMaTWNRuADOaocYiHidgNkNDz8SCekpdNJ7lqceFcBT2zjumnb28/o7IMaNROpLBZdQkLkJXSeaQWGqn3kog==} - hasBin: true - lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -13225,49 +13168,6 @@ snapshots: leac@0.6.0: {} - lefthook-darwin-arm64@1.8.4: - optional: true - - lefthook-darwin-x64@1.8.4: - optional: true - - lefthook-freebsd-arm64@1.8.4: - optional: true - - lefthook-freebsd-x64@1.8.4: - optional: true - - lefthook-linux-arm64@1.8.4: - optional: true - - lefthook-linux-x64@1.8.4: - optional: true - - lefthook-openbsd-arm64@1.8.4: - optional: true - - lefthook-openbsd-x64@1.8.4: - optional: true - - lefthook-windows-arm64@1.8.4: - optional: true - - lefthook-windows-x64@1.8.4: - optional: true - - lefthook@1.8.4: - optionalDependencies: - lefthook-darwin-arm64: 1.8.4 - lefthook-darwin-x64: 1.8.4 - lefthook-freebsd-arm64: 1.8.4 - lefthook-freebsd-x64: 1.8.4 - lefthook-linux-arm64: 1.8.4 - lefthook-linux-x64: 1.8.4 - lefthook-openbsd-arm64: 1.8.4 - lefthook-openbsd-x64: 1.8.4 - lefthook-windows-arm64: 1.8.4 - lefthook-windows-x64: 1.8.4 - lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} From c93ec1f06cff35cdce4ccad67712114bcf22fd49 Mon Sep 17 00:00:00 2001 From: bdkopen Date: Thu, 15 Jan 2026 21:44:00 -0500 Subject: [PATCH 08/40] chore: uninstall disabled `@commitlint/cli` and `@commitlint/config-conventional` package --- apps/dokploy/package.json | 5 - package.json | 7 - pnpm-lock.yaml | 559 -------------------------------------- 3 files changed, 571 deletions(-) diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index dbf8493e3..7fe165a42 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -196,10 +196,5 @@ "*": [ "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true" ] - }, - "commitlint": { - "extends": [ - "@commitlint/config-conventional" - ] } } diff --git a/package.json b/package.json index 7cd56ce15..9a920c59c 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,6 @@ }, "devDependencies": { "@biomejs/biome": "2.1.1", - "@commitlint/cli": "^19.8.1", - "@commitlint/config-conventional": "^19.8.1", "@types/node": "^18.19.104", "dotenv": "16.4.5", "esbuild": "0.20.2", @@ -42,11 +40,6 @@ "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true" ] }, - "commitlint": { - "extends": [ - "@commitlint/config-conventional" - ] - }, "resolutions": { "@types/react": "18.3.5", "@types/react-dom": "18.3.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6cc147e8b..d5fc7f074 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,12 +15,6 @@ importers: '@biomejs/biome': specifier: 2.1.1 version: 2.1.1 - '@commitlint/cli': - specifier: ^19.8.1 - version: 19.8.1(@types/node@18.19.104)(typescript@5.8.3) - '@commitlint/config-conventional': - specifier: ^19.8.1 - version: 19.8.1 '@types/node': specifier: ^18.19.104 version: 18.19.104 @@ -876,14 +870,6 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} - engines: {node: '>=6.9.0'} - '@babel/runtime-corejs3@7.27.3': resolution: {integrity: sha512-ZYcgrwb+dkWNcDlsTe4fH1CMdqMDSJ5lWFd1by8Si2pI54XcQjte/+ViIPqAk7EAWisaUxvQ89grv+bNX2x8zg==} engines: {node: '>=6.9.0'} @@ -999,75 +985,6 @@ packages: '@codemirror/view@6.36.8': resolution: {integrity: sha512-yoRo4f+FdnD01fFt4XpfpMCcCAo9QvZOtbrXExn4SqzH32YC6LgzqxfLZw/r6Ge65xyY03mK/UfUqrVw1gFiFg==} - '@commitlint/cli@19.8.1': - resolution: {integrity: sha512-LXUdNIkspyxrlV6VDHWBmCZRtkEVRpBKxi2Gtw3J54cGWhLCTouVD/Q6ZSaSvd2YaDObWK8mDjrz3TIKtaQMAA==} - engines: {node: '>=v18'} - hasBin: true - - '@commitlint/config-conventional@19.8.1': - resolution: {integrity: sha512-/AZHJL6F6B/G959CsMAzrPKKZjeEiAVifRyEwXxcT6qtqbPwGw+iQxmNS+Bu+i09OCtdNRW6pNpBvgPrtMr9EQ==} - engines: {node: '>=v18'} - - '@commitlint/config-validator@19.8.1': - resolution: {integrity: sha512-0jvJ4u+eqGPBIzzSdqKNX1rvdbSU1lPNYlfQQRIFnBgLy26BtC0cFnr7c/AyuzExMxWsMOte6MkTi9I3SQ3iGQ==} - engines: {node: '>=v18'} - - '@commitlint/ensure@19.8.1': - resolution: {integrity: sha512-mXDnlJdvDzSObafjYrOSvZBwkD01cqB4gbnnFuVyNpGUM5ijwU/r/6uqUmBXAAOKRfyEjpkGVZxaDsCVnHAgyw==} - engines: {node: '>=v18'} - - '@commitlint/execute-rule@19.8.1': - resolution: {integrity: sha512-YfJyIqIKWI64Mgvn/sE7FXvVMQER/Cd+s3hZke6cI1xgNT/f6ZAz5heND0QtffH+KbcqAwXDEE1/5niYayYaQA==} - engines: {node: '>=v18'} - - '@commitlint/format@19.8.1': - resolution: {integrity: sha512-kSJj34Rp10ItP+Eh9oCItiuN/HwGQMXBnIRk69jdOwEW9llW9FlyqcWYbHPSGofmjsqeoxa38UaEA5tsbm2JWw==} - engines: {node: '>=v18'} - - '@commitlint/is-ignored@19.8.1': - resolution: {integrity: sha512-AceOhEhekBUQ5dzrVhDDsbMaY5LqtN8s1mqSnT2Kz1ERvVZkNihrs3Sfk1Je/rxRNbXYFzKZSHaPsEJJDJV8dg==} - engines: {node: '>=v18'} - - '@commitlint/lint@19.8.1': - resolution: {integrity: sha512-52PFbsl+1EvMuokZXLRlOsdcLHf10isTPlWwoY1FQIidTsTvjKXVXYb7AvtpWkDzRO2ZsqIgPK7bI98x8LRUEw==} - engines: {node: '>=v18'} - - '@commitlint/load@19.8.1': - resolution: {integrity: sha512-9V99EKG3u7z+FEoe4ikgq7YGRCSukAcvmKQuTtUyiYPnOd9a2/H9Ak1J9nJA1HChRQp9OA/sIKPugGS+FK/k1A==} - engines: {node: '>=v18'} - - '@commitlint/message@19.8.1': - resolution: {integrity: sha512-+PMLQvjRXiU+Ae0Wc+p99EoGEutzSXFVwQfa3jRNUZLNW5odZAyseb92OSBTKCu+9gGZiJASt76Cj3dLTtcTdg==} - engines: {node: '>=v18'} - - '@commitlint/parse@19.8.1': - resolution: {integrity: sha512-mmAHYcMBmAgJDKWdkjIGq50X4yB0pSGpxyOODwYmoexxxiUCy5JJT99t1+PEMK7KtsCtzuWYIAXYAiKR+k+/Jw==} - engines: {node: '>=v18'} - - '@commitlint/read@19.8.1': - resolution: {integrity: sha512-03Jbjb1MqluaVXKHKRuGhcKWtSgh3Jizqy2lJCRbRrnWpcM06MYm8th59Xcns8EqBYvo0Xqb+2DoZFlga97uXQ==} - engines: {node: '>=v18'} - - '@commitlint/resolve-extends@19.8.1': - resolution: {integrity: sha512-GM0mAhFk49I+T/5UCYns5ayGStkTt4XFFrjjf0L4S26xoMTSkdCf9ZRO8en1kuopC4isDFuEm7ZOm/WRVeElVg==} - engines: {node: '>=v18'} - - '@commitlint/rules@19.8.1': - resolution: {integrity: sha512-Hnlhd9DyvGiGwjfjfToMi1dsnw1EXKGJNLTcsuGORHz6SS9swRgkBsou33MQ2n51/boIDrbsg4tIBbRpEWK2kw==} - engines: {node: '>=v18'} - - '@commitlint/to-lines@19.8.1': - resolution: {integrity: sha512-98Mm5inzbWTKuZQr2aW4SReY6WUukdWXuZhrqf1QdKPZBCCsXuG87c+iP0bwtD6DBnmVVQjgp4whoHRVixyPBg==} - engines: {node: '>=v18'} - - '@commitlint/top-level@19.8.1': - resolution: {integrity: sha512-Ph8IN1IOHPSDhURCSXBz44+CIu+60duFwRsg6HqaISFHQHbmBtxVw4ZrFNIYUzEP7WwrNPxa2/5qJ//NK1FGcw==} - engines: {node: '>=v18'} - - '@commitlint/types@19.8.1': - resolution: {integrity: sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw==} - engines: {node: '>=v18'} - '@dokploy/trpc-openapi@0.0.4': resolution: {integrity: sha512-a7VKunKu9arq57bP9MPH7ikJuKfT5SILnNy70vMqf1stm5IrqMG3Y7CIFprFe0DZiw3bwjue0KpETIATBftN6w==} peerDependencies: @@ -3929,9 +3846,6 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - '@types/conventional-commits-parser@5.0.1': - resolution: {integrity: sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==} - '@types/d3-array@3.2.1': resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} @@ -4149,10 +4063,6 @@ packages: '@xterm/xterm@5.5.0': resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==} - JSONStream@1.3.5: - resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} - hasBin: true - abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -4212,9 +4122,6 @@ packages: peerDependencies: zod: ^3.25.76 || ^4 - ajv@8.17.1: - resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} @@ -4277,9 +4184,6 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} - array-ify@1.0.0: - resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} - array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -4420,10 +4324,6 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - camelcase-css@2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} @@ -4592,9 +4492,6 @@ packages: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} - compare-func@2.0.0: - resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} - concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -4611,19 +4508,6 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} - conventional-changelog-angular@7.0.0: - resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} - engines: {node: '>=16'} - - conventional-changelog-conventionalcommits@7.0.2: - resolution: {integrity: sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==} - engines: {node: '>=16'} - - conventional-commits-parser@5.0.0: - resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==} - engines: {node: '>=16'} - hasBin: true - cookie-es@1.2.2: resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} @@ -4640,23 +4524,6 @@ packages: core-js@3.42.0: resolution: {integrity: sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==} - cosmiconfig-typescript-loader@6.1.0: - resolution: {integrity: sha512-tJ1w35ZRUiM5FeTzT7DtYWAFFv37ZLqSRkGi2oeCK1gPhvaWjkAtfXvLmvE1pRfxxp9aQo6ba/Pvg1dKj05D4g==} - engines: {node: '>=v18'} - peerDependencies: - '@types/node': '*' - cosmiconfig: '>=9' - typescript: '>=5' - - cosmiconfig@9.0.0: - resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=4.9.5' - peerDependenciesMeta: - typescript: - optional: true - cpu-features@0.0.10: resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} engines: {node: '>=10.0.0'} @@ -4739,10 +4606,6 @@ packages: resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} engines: {node: '>=12'} - dargs@8.1.0: - resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} - engines: {node: '>=12'} - date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} @@ -4886,10 +4749,6 @@ packages: domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - dot-prop@5.3.0: - resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} - engines: {node: '>=8'} - dotenv@16.4.5: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} @@ -5033,10 +4892,6 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - env-paths@2.2.1: - resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} - engines: {node: '>=6'} - env-paths@3.0.0: resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -5045,9 +4900,6 @@ packages: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} - error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -5143,9 +4995,6 @@ packages: fast-deep-equal@2.0.1: resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-equals@5.2.2: resolution: {integrity: sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==} engines: {node: '>=6.0.0'} @@ -5164,9 +5013,6 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fast-uri@3.0.6: - resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} - fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -5181,10 +5027,6 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} - find-up@7.0.0: - resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} - engines: {node: '>=18'} - follow-redirects@1.15.9: resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} engines: {node: '>=4.0'} @@ -5294,11 +5136,6 @@ packages: get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} - git-raw-commits@4.0.0: - resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} - engines: {node: '>=16'} - hasBin: true - glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -5315,10 +5152,6 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported - global-directory@4.0.1: - resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} - engines: {node: '>=18'} - globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -5455,16 +5288,9 @@ packages: resolution: {integrity: sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==} engines: {node: '>=0.10.0'} - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - import-in-the-middle@1.14.2: resolution: {integrity: sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw==} - import-meta-resolve@4.1.0: - resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} - indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} @@ -5487,10 +5313,6 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - ini@4.1.1: - resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - inline-style-parser@0.2.4: resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} @@ -5566,9 +5388,6 @@ packages: is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -5620,10 +5439,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-obj@2.0.0: - resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} - engines: {node: '>=8'} - is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} @@ -5636,10 +5451,6 @@ packages: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - is-text-path@2.0.0: - resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} - engines: {node: '>=8'} - is-what@4.1.16: resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} engines: {node: '>=12.13'} @@ -5658,10 +5469,6 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true - jiti@2.4.2: - resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} - hasBin: true - jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} @@ -5700,22 +5507,12 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - jsonparse@1.3.1: - resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} - engines: {'0': node >= 0.2.0} - jsonwebtoken@9.0.2: resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} engines: {node: '>=12', npm: '>=6'} @@ -5802,10 +5599,6 @@ packages: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} - locate-path@7.2.0: - resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} @@ -5842,30 +5635,12 @@ packages: lodash.isstring@4.0.1: resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} - lodash.kebabcase@4.1.1: - resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} - lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.mergewith@4.6.2: - resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} - lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} - lodash.snakecase@4.1.1: - resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} - - lodash.startcase@4.4.0: - resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} - - lodash.uniq@4.5.0: - resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} - - lodash.upperfirst@4.3.1: - resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} - lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -5961,10 +5736,6 @@ packages: resolution: {integrity: sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg==} engines: {node: '>= 4.0.0'} - meow@12.1.1: - resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} - engines: {node: '>=16.10'} - merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} @@ -6366,10 +6137,6 @@ packages: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} - p-limit@4.0.0: - resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - p-limit@5.0.0: resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} engines: {node: '>=18'} @@ -6378,10 +6145,6 @@ packages: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} - p-locate@6.0.0: - resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -6389,20 +6152,12 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - parse-entities@2.0.0: resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - parseley@0.12.1: resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} @@ -6414,10 +6169,6 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-exists@5.0.0: - resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -6922,10 +6673,6 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - require-in-the-middle@7.5.2: resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} engines: {node: '>=8.6.0'} @@ -6942,14 +6689,6 @@ packages: resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -7297,10 +7036,6 @@ packages: temporal-spec@0.2.4: resolution: {integrity: sha512-lDMFv4nKQrSjlkHKAlHVqKrBG4DyFfa9F74cmBZ3Iy3ed8yvWnlWSIdi4IKfSqwmazAohBNwiN64qGx4y5Q3IQ==} - text-extensions@2.4.0: - resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} - engines: {node: '>=8'} - theming@3.3.0: resolution: {integrity: sha512-u6l4qTJRDaWZsqa8JugaNt7Xd8PPl9+gonZaIe28vAhqgHMIG/DOyFPqiKN/gQLQYj05tHv+YQdNILL4zoiAVA==} engines: {node: '>=8'} @@ -7323,9 +7058,6 @@ packages: thread-stream@3.1.0: resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} - through@2.3.8: - resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -7335,9 +7067,6 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.0.1: - resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} - tinypool@0.8.4: resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} engines: {node: '>=14.0.0'} @@ -7470,10 +7199,6 @@ packages: resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} engines: {node: '>=18.17'} - unicorn-magic@0.1.0: - resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} - engines: {node: '>=18'} - unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -7857,14 +7582,6 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@babel/code-frame@7.27.1': - dependencies: - '@babel/helper-validator-identifier': 7.27.1 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/helper-validator-identifier@7.27.1': {} - '@babel/runtime-corejs3@7.27.3': dependencies: core-js-pure: 3.42.0 @@ -8001,116 +7718,6 @@ snapshots: style-mod: 4.1.2 w3c-keyname: 2.2.8 - '@commitlint/cli@19.8.1(@types/node@18.19.104)(typescript@5.8.3)': - dependencies: - '@commitlint/format': 19.8.1 - '@commitlint/lint': 19.8.1 - '@commitlint/load': 19.8.1(@types/node@18.19.104)(typescript@5.8.3) - '@commitlint/read': 19.8.1 - '@commitlint/types': 19.8.1 - tinyexec: 1.0.1 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - typescript - - '@commitlint/config-conventional@19.8.1': - dependencies: - '@commitlint/types': 19.8.1 - conventional-changelog-conventionalcommits: 7.0.2 - - '@commitlint/config-validator@19.8.1': - dependencies: - '@commitlint/types': 19.8.1 - ajv: 8.17.1 - - '@commitlint/ensure@19.8.1': - dependencies: - '@commitlint/types': 19.8.1 - lodash.camelcase: 4.3.0 - lodash.kebabcase: 4.1.1 - lodash.snakecase: 4.1.1 - lodash.startcase: 4.4.0 - lodash.upperfirst: 4.3.1 - - '@commitlint/execute-rule@19.8.1': {} - - '@commitlint/format@19.8.1': - dependencies: - '@commitlint/types': 19.8.1 - chalk: 5.4.1 - - '@commitlint/is-ignored@19.8.1': - dependencies: - '@commitlint/types': 19.8.1 - semver: 7.7.3 - - '@commitlint/lint@19.8.1': - dependencies: - '@commitlint/is-ignored': 19.8.1 - '@commitlint/parse': 19.8.1 - '@commitlint/rules': 19.8.1 - '@commitlint/types': 19.8.1 - - '@commitlint/load@19.8.1(@types/node@18.19.104)(typescript@5.8.3)': - dependencies: - '@commitlint/config-validator': 19.8.1 - '@commitlint/execute-rule': 19.8.1 - '@commitlint/resolve-extends': 19.8.1 - '@commitlint/types': 19.8.1 - chalk: 5.4.1 - cosmiconfig: 9.0.0(typescript@5.8.3) - cosmiconfig-typescript-loader: 6.1.0(@types/node@18.19.104)(cosmiconfig@9.0.0(typescript@5.8.3))(typescript@5.8.3) - lodash.isplainobject: 4.0.6 - lodash.merge: 4.6.2 - lodash.uniq: 4.5.0 - transitivePeerDependencies: - - '@types/node' - - typescript - - '@commitlint/message@19.8.1': {} - - '@commitlint/parse@19.8.1': - dependencies: - '@commitlint/types': 19.8.1 - conventional-changelog-angular: 7.0.0 - conventional-commits-parser: 5.0.0 - - '@commitlint/read@19.8.1': - dependencies: - '@commitlint/top-level': 19.8.1 - '@commitlint/types': 19.8.1 - git-raw-commits: 4.0.0 - minimist: 1.2.8 - tinyexec: 1.0.1 - - '@commitlint/resolve-extends@19.8.1': - dependencies: - '@commitlint/config-validator': 19.8.1 - '@commitlint/types': 19.8.1 - global-directory: 4.0.1 - import-meta-resolve: 4.1.0 - lodash.mergewith: 4.6.2 - resolve-from: 5.0.0 - - '@commitlint/rules@19.8.1': - dependencies: - '@commitlint/ensure': 19.8.1 - '@commitlint/message': 19.8.1 - '@commitlint/to-lines': 19.8.1 - '@commitlint/types': 19.8.1 - - '@commitlint/to-lines@19.8.1': {} - - '@commitlint/top-level@19.8.1': - dependencies: - find-up: 7.0.0 - - '@commitlint/types@19.8.1': - dependencies: - '@types/conventional-commits-parser': 5.0.1 - chalk: 5.4.1 - '@dokploy/trpc-openapi@0.0.4(@trpc/server@10.45.2)(@types/node@18.19.104)(zod@3.25.32)': dependencies: '@trpc/server': 10.45.2 @@ -11197,10 +10804,6 @@ snapshots: dependencies: '@types/node': 20.17.51 - '@types/conventional-commits-parser@5.0.1': - dependencies: - '@types/node': 20.17.51 - '@types/d3-array@3.2.1': {} '@types/d3-color@3.1.3': {} @@ -11456,11 +11059,6 @@ snapshots: '@xterm/xterm@5.5.0': {} - JSONStream@1.3.5: - dependencies: - jsonparse: 1.3.1 - through: 2.3.8 - abbrev@1.1.1: {} abbrev@2.0.0: {} @@ -11521,13 +11119,6 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 3.25.32 - ajv@8.17.1: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.0.6 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - ansi-align@3.0.1: dependencies: string-width: 4.2.3 @@ -11578,8 +11169,6 @@ snapshots: dependencies: tslib: 2.8.1 - array-ify@1.0.0: {} - array-union@2.1.0: {} asn1@0.2.6: @@ -11771,8 +11360,6 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 - callsites@3.1.0: {} - camelcase-css@2.0.1: {} camelcase@5.3.1: {} @@ -11931,11 +11518,6 @@ snapshots: commander@9.5.0: {} - compare-func@2.0.0: - dependencies: - array-ify: 1.0.0 - dot-prop: 5.3.0 - concat-map@0.0.1: {} confbox@0.1.8: {} @@ -11951,21 +11533,6 @@ snapshots: dependencies: safe-buffer: 5.2.1 - conventional-changelog-angular@7.0.0: - dependencies: - compare-func: 2.0.0 - - conventional-changelog-conventionalcommits@7.0.2: - dependencies: - compare-func: 2.0.0 - - conventional-commits-parser@5.0.0: - dependencies: - JSONStream: 1.3.5 - is-text-path: 2.0.0 - meow: 12.1.1 - split2: 4.2.0 - cookie-es@1.2.2: {} copy-anything@3.0.5: @@ -11980,22 +11547,6 @@ snapshots: core-js@3.42.0: {} - cosmiconfig-typescript-loader@6.1.0(@types/node@18.19.104)(cosmiconfig@9.0.0(typescript@5.8.3))(typescript@5.8.3): - dependencies: - '@types/node': 18.19.104 - cosmiconfig: 9.0.0(typescript@5.8.3) - jiti: 2.4.2 - typescript: 5.8.3 - - cosmiconfig@9.0.0(typescript@5.8.3): - dependencies: - env-paths: 2.2.1 - import-fresh: 3.3.1 - js-yaml: 4.1.0 - parse-json: 5.2.0 - optionalDependencies: - typescript: 5.8.3 - cpu-features@0.0.10: dependencies: buildcheck: 0.0.6 @@ -12079,8 +11630,6 @@ snapshots: d3-timer@3.0.1: {} - dargs@8.1.0: {} - date-fns@3.6.0: {} dateformat@4.6.3: {} @@ -12204,10 +11753,6 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 - dot-prop@5.3.0: - dependencies: - is-obj: 2.0.0 - dotenv@16.4.5: {} drange@1.1.1: {} @@ -12271,16 +11816,10 @@ snapshots: entities@4.5.0: {} - env-paths@2.2.1: {} - env-paths@3.0.0: {} environment@1.1.0: {} - error-ex@1.3.2: - dependencies: - is-arrayish: 0.2.1 - es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -12452,8 +11991,6 @@ snapshots: fast-deep-equal@2.0.1: {} - fast-deep-equal@3.1.3: {} - fast-equals@5.2.2: {} fast-glob@3.3.3: @@ -12470,8 +12007,6 @@ snapshots: fast-safe-stringify@2.1.1: {} - fast-uri@3.0.6: {} - fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -12489,12 +12024,6 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 - find-up@7.0.0: - dependencies: - locate-path: 7.2.0 - path-exists: 5.0.0 - unicorn-magic: 0.1.0 - follow-redirects@1.15.9: {} foreground-child@3.3.1: @@ -12611,12 +12140,6 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - git-raw-commits@4.0.0: - dependencies: - dargs: 8.1.0 - meow: 12.1.1 - split2: 4.2.0 - glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -12643,10 +12166,6 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 - global-directory@4.0.1: - dependencies: - ini: 4.1.1 - globby@11.1.0: dependencies: array-union: 2.1.0 @@ -12825,11 +12344,6 @@ snapshots: immutable@3.8.2: {} - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - import-in-the-middle@1.14.2: dependencies: acorn: 8.14.1 @@ -12837,8 +12351,6 @@ snapshots: cjs-module-lexer: 1.4.3 module-details-from-path: 1.0.4 - import-meta-resolve@4.1.0: {} - indent-string@4.0.0: {} indent-string@5.0.0: {} @@ -12854,8 +12366,6 @@ snapshots: ini@1.3.8: {} - ini@4.1.1: {} - inline-style-parser@0.2.4: {} inngest@3.40.1(h3@1.15.3)(hono@4.7.10)(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.8.3): @@ -12934,8 +12444,6 @@ snapshots: is-alphabetical: 2.0.1 is-decimal: 2.0.1 - is-arrayish@0.2.1: {} - is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -12974,18 +12482,12 @@ snapshots: is-number@7.0.0: {} - is-obj@2.0.0: {} - is-plain-obj@4.1.0: {} is-stream@2.0.1: {} is-stream@3.0.0: {} - is-text-path@2.0.0: - dependencies: - text-extensions: 2.4.0 - is-what@4.1.16: {} isexe@2.0.0: {} @@ -13000,8 +12502,6 @@ snapshots: jiti@1.21.7: {} - jiti@2.4.2: {} - jose@5.10.0: {} joycon@3.1.1: {} @@ -13034,16 +12534,10 @@ snapshots: json-buffer@3.0.1: {} - json-parse-even-better-errors@2.3.1: {} - - json-schema-traverse@1.0.0: {} - json-schema@0.4.0: {} json-stringify-safe@5.0.1: {} - jsonparse@1.3.1: {} - jsonwebtoken@9.0.2: dependencies: jws: 3.2.2 @@ -13205,10 +12699,6 @@ snapshots: dependencies: p-locate: 4.1.0 - locate-path@7.2.0: - dependencies: - p-locate: 6.0.0 - lodash.camelcase@4.3.0: {} lodash.castarray@4.4.0: {} @@ -13233,22 +12723,10 @@ snapshots: lodash.isstring@4.0.1: {} - lodash.kebabcase@4.1.1: {} - lodash.merge@4.6.2: {} - lodash.mergewith@4.6.2: {} - lodash.once@4.1.1: {} - lodash.snakecase@4.1.1: {} - - lodash.startcase@4.4.0: {} - - lodash.uniq@4.5.0: {} - - lodash.upperfirst@4.3.1: {} - lodash@4.17.21: {} log-update@6.1.0: @@ -13403,8 +12881,6 @@ snapshots: tree-dump: 1.0.3(tslib@2.8.1) tslib: 2.8.1 - meow@12.1.1: {} - merge-descriptors@1.0.3: {} merge-stream@2.0.0: {} @@ -13842,10 +13318,6 @@ snapshots: dependencies: p-try: 2.2.0 - p-limit@4.0.0: - dependencies: - yocto-queue: 1.2.1 - p-limit@5.0.0: dependencies: yocto-queue: 1.2.1 @@ -13854,18 +13326,10 @@ snapshots: dependencies: p-limit: 2.3.0 - p-locate@6.0.0: - dependencies: - p-limit: 4.0.0 - p-try@2.2.0: {} package-json-from-dist@1.0.1: {} - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - parse-entities@2.0.0: dependencies: character-entities: 1.2.4 @@ -13885,13 +13349,6 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 - parse-json@5.2.0: - dependencies: - '@babel/code-frame': 7.27.1 - error-ex: 1.3.2 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - parseley@0.12.1: dependencies: leac: 0.6.0 @@ -13901,8 +13358,6 @@ snapshots: path-exists@4.0.0: {} - path-exists@5.0.0: {} - path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -14445,8 +13900,6 @@ snapshots: require-directory@2.1.1: {} - require-from-string@2.0.2: {} - require-in-the-middle@7.5.2: dependencies: debug: 4.4.1 @@ -14463,10 +13916,6 @@ snapshots: resolve-alpn@1.2.1: {} - resolve-from@4.0.0: {} - - resolve-from@5.0.0: {} - resolve-pkg-maps@1.0.0: {} resolve@1.22.10: @@ -14921,8 +14370,6 @@ snapshots: temporal-spec@0.2.4: {} - text-extensions@2.4.0: {} - theming@3.3.0(react@18.2.0): dependencies: hoist-non-react-statics: 3.3.2 @@ -14947,16 +14394,12 @@ snapshots: dependencies: real-require: 0.2.0 - through@2.3.8: {} - tiny-invariant@1.3.3: {} tiny-warning@1.0.3: {} tinybench@2.9.0: {} - tinyexec@1.0.1: {} - tinypool@0.8.4: {} tinyspy@2.2.1: {} @@ -15060,8 +14503,6 @@ snapshots: undici@6.21.3: {} - unicorn-magic@0.1.0: {} - unified@11.0.5: dependencies: '@types/unist': 3.0.3 From 384fdd01d69941a7be572706e069ce65b51b1efc Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Fri, 16 Jan 2026 01:05:40 -0600 Subject: [PATCH 09/40] feat(server): add monitoring configuration for cloud setup --- packages/server/src/setup/server-setup.ts | 41 +++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/server/src/setup/server-setup.ts b/packages/server/src/setup/server-setup.ts index 7508fadea..376855e1d 100644 --- a/packages/server/src/setup/server-setup.ts +++ b/packages/server/src/setup/server-setup.ts @@ -1,10 +1,14 @@ import path from "node:path"; -import { paths } from "@dokploy/server/constants"; +import { IS_CLOUD, paths } from "@dokploy/server/constants"; +import { getDokployUrl } from "@dokploy/server/services/admin"; import { createServerDeployment, updateDeploymentStatus, } from "@dokploy/server/services/deployment"; -import { findServerById } from "@dokploy/server/services/server"; +import { + findServerById, + updateServerById, +} from "@dokploy/server/services/server"; import { getDefaultMiddlewares, getDefaultServerTraefikConfig, @@ -16,6 +20,7 @@ import { import slug from "slugify"; import { Client } from "ssh2"; import { recreateDirectory } from "../utils/filesystem/directory"; +import { setupMonitoring } from "./monitoring-setup"; export const slugify = (text: string | undefined) => { if (!text) { @@ -59,6 +64,38 @@ export const serverSetup = async ( ); await installRequirements(serverId, onData); + if (IS_CLOUD) { + onData?.("\nConfiguring Monitoring: 🔄\n"); + + // Generate token and configure monitoring + const generateToken = () => { + const array = new Uint8Array(64); + crypto.getRandomValues(array); + return Array.from(array, (byte) => + byte.toString(16).padStart(2, "0"), + ).join(""); + }; + + const baseUrl = await getDokployUrl(); + const token = generateToken(); + const urlCallback = `${baseUrl}/api/trpc/notification.receiveNotification`; + + // Update server with monitoring configuration + await updateServerById(serverId, { + metricsConfig: { + server: { + ...server.metricsConfig.server, + token: token, + urlCallback: urlCallback, + }, + containers: server.metricsConfig.containers, + }, + }); + + await setupMonitoring(serverId); + onData?.("\nMonitoring Configured: ✅\n"); + } + await updateDeploymentStatus(deployment.deploymentId, "done"); onData?.("\nSetup Server: ✅\n"); From 79655b567368b03b5a2e3474491f16b55ea9574d Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Fri, 16 Jan 2026 01:07:17 -0600 Subject: [PATCH 10/40] refactor(server): move token generation function to a separate utility for better organization --- packages/server/src/setup/server-setup.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/server/src/setup/server-setup.ts b/packages/server/src/setup/server-setup.ts index 376855e1d..32e5e4a7e 100644 --- a/packages/server/src/setup/server-setup.ts +++ b/packages/server/src/setup/server-setup.ts @@ -22,6 +22,14 @@ import { Client } from "ssh2"; import { recreateDirectory } from "../utils/filesystem/directory"; import { setupMonitoring } from "./monitoring-setup"; +const generateToken = () => { + const array = new Uint8Array(64); + crypto.getRandomValues(array); + return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join( + "", + ); +}; + export const slugify = (text: string | undefined) => { if (!text) { return ""; @@ -67,15 +75,6 @@ export const serverSetup = async ( if (IS_CLOUD) { onData?.("\nConfiguring Monitoring: 🔄\n"); - // Generate token and configure monitoring - const generateToken = () => { - const array = new Uint8Array(64); - crypto.getRandomValues(array); - return Array.from(array, (byte) => - byte.toString(16).padStart(2, "0"), - ).join(""); - }; - const baseUrl = await getDokployUrl(); const token = generateToken(); const urlCallback = `${baseUrl}/api/trpc/notification.receiveNotification`; From f0400495b0967312e72e4183573c9cab22245d5e Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Fri, 16 Jan 2026 01:18:14 -0600 Subject: [PATCH 11/40] refactor(README): restructure table --- README.md | 60 +++++++++++++------------------------------------------ 1 file changed, 14 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 23fcd0c9d..e97735597 100644 --- a/README.md +++ b/README.md @@ -68,53 +68,21 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com). [Github Sponsors](https://github.com/sponsors/Siumauricio) - +## Sponsors - - -### Hero Sponsors 🎖 - -
- Hostinger - LX Aer - - - - - - -
- - - - - -### Premium Supporters 🥇 - -
- Supafort.com - agentdock.ai -
- - - - - -### Elite Contributors 🥈 - -
- AmericanCloud - Tolgee -
- -### Supporting Members 🥉 - -
- - Cloudblast.io - - Synexa -
+| Sponsor | Logo | Supporter Level | +|---------|:----:|----------------| +| [Hostinger](https://www.hostinger.com/vps-hosting?ref=dokploy) | Hostinger | 🎖 Hero Sponsor | +| [LX Aer](https://www.lxaer.com/?ref=dokploy) | LX Aer | 🎖 Hero Sponsor | +| [LinkDR](https://linkdr.com/?ref=dokploy) | LinkDR | 🎖 Hero Sponsor | +| [LambdaTest](https://www.lambdatest.com/?utm_source=dokploy&utm_medium=sponsor) | LambdaTest | 🎖 Hero Sponsor | +| [Awesome Tools](https://awesome.tools/) | Awesome Tools | 🎖 Hero Sponsor | +| [Supafort](https://supafort.com/?ref=dokploy) | Supafort.com | 🥇 Premium Supporter | +| [Agentdock](https://agentdock.ai/?ref=dokploy) | agentdock.ai | 🥇 Premium Supporter | +| [AmericanCloud](https://americancloud.com/?ref=dokploy) | AmericanCloud | 🥈 Elite Contributor | +| [Tolgee](https://tolgee.io/?utm_source=github_dokploy&utm_medium=banner&utm_campaign=dokploy) | Tolgee | 🥈 Elite Contributor | +| [Cloudblast](https://cloudblast.io/?ref=dokploy) | Cloudblast.io | 🥉 Supporting Member | +| [Synexa](https://synexa.ai/?ref=dokploy) | Synexa | 🥉 Supporting Member | ### Community Backers 🤝 From 138b1935778e6b811665083e2b41d5bc2381e46f Mon Sep 17 00:00:00 2001 From: Bima42 Date: Mon, 19 Jan 2026 08:51:58 +0100 Subject: [PATCH 12/40] feat: make projects clickable in breadcrumbs --- .../[environmentId]/services/application/[applicationId].tsx | 1 + .../environment/[environmentId]/services/compose/[composeId].tsx | 1 + .../environment/[environmentId]/services/mariadb/[mariadbId].tsx | 1 + .../environment/[environmentId]/services/mongo/[mongoId].tsx | 1 + .../environment/[environmentId]/services/mysql/[mysqlId].tsx | 1 + .../[environmentId]/services/postgres/[postgresId].tsx | 1 + .../environment/[environmentId]/services/redis/[redisId].tsx | 1 + 7 files changed, 7 insertions(+) diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/application/[applicationId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/application/[applicationId].tsx index 2be9e5edf..7917bd97c 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/application/[applicationId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/application/[applicationId].tsx @@ -108,6 +108,7 @@ const Service = ( { name: "Projects", href: "/dashboard/projects" }, { name: data?.environment?.project?.name || "", + href: `/dashboard/project/${projectId}/environment/${environmentId}`, }, { name: data?.environment?.name || "", diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx index b03392c45..1d6902c59 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx @@ -97,6 +97,7 @@ const Service = ( { name: "Projects", href: "/dashboard/projects" }, { name: data?.environment?.project?.name || "", + href: `/dashboard/project/${projectId}/environment/${environmentId}`, }, { name: data?.environment?.name || "", diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx index e496dd928..0a1e8501d 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx @@ -79,6 +79,7 @@ const Mariadb = ( { name: "Projects", href: "/dashboard/projects" }, { name: data?.environment?.project?.name || "", + href: `/dashboard/project/${projectId}/environment/${environmentId}`, }, { name: data?.environment?.name || "", diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx index 077add05b..bae83cb2b 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx @@ -78,6 +78,7 @@ const Mongo = ( { name: "Projects", href: "/dashboard/projects" }, { name: data?.environment?.project?.name || "", + href: `/dashboard/project/${projectId}/environment/${environmentId}`, }, { name: data?.environment?.name || "", diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx index acf7280aa..ba2b9d8a0 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx @@ -77,6 +77,7 @@ const MySql = ( { name: "Projects", href: "/dashboard/projects" }, { name: data?.environment?.project?.name || "", + href: `/dashboard/project/${projectId}/environment/${environmentId}`, }, { name: data?.environment?.name || "", diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx index d8bd94ca2..1d90e3e13 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx @@ -77,6 +77,7 @@ const Postgresql = ( { name: "Projects", href: "/dashboard/projects" }, { name: data?.environment?.project?.name || "", + href: `/dashboard/project/${projectId}/environment/${environmentId}`, }, { name: data?.environment?.name || "", diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx index 0f4bd4a88..47eb82a74 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx @@ -77,6 +77,7 @@ const Redis = ( { name: "Projects", href: "/dashboard/projects" }, { name: data?.environment?.project?.name || "", + href: `/dashboard/project/${projectId}/environment/${environmentId}`, }, { name: data?.environment?.name || "", From a33c6bcce441c302e16fb017e85a2a76c01fb5f0 Mon Sep 17 00:00:00 2001 From: Mika Andrianarijaona Date: Tue, 20 Jan 2026 11:51:50 +0100 Subject: [PATCH 13/40] fix: truncate project card title to avoid ellise shift Fixes #3483 --- 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 a618a20ac..c962053b3 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -419,7 +419,7 @@ export const ShowProjects = () => { ) : null} - +
From e5fcc10db2a246b9a1e7ff0ec552b7b0a4af690b Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 20 Jan 2026 16:01:43 +0100 Subject: [PATCH 14/40] feat(cluster): implement advanced swarm settings forms - Added multiple forms for managing swarm settings including Health Check, Restart Policy, Placement, Update Config, Rollback Config, Mode, Labels, Stop Grace Period, and Endpoint Spec. - Introduced utility functions for filtering empty values and checking for values to save. - Enhanced the UI for better navigation and form handling within the dashboard. - Integrated form validation using Zod and React Hook Form for improved user experience. --- .../cluster/modify-swarm-settings.tsx | 1006 ++--------------- .../swarm-forms/endpoint-spec-form.tsx | 151 +++ .../cluster/swarm-forms/health-check-form.tsx | 267 +++++ .../advanced/cluster/swarm-forms/index.ts | 10 + .../cluster/swarm-forms/labels-form.tsx | 199 ++++ .../cluster/swarm-forms/mode-form.tsx | 195 ++++ .../cluster/swarm-forms/placement-form.tsx | 342 ++++++ .../swarm-forms/restart-policy-form.tsx | 219 ++++ .../swarm-forms/rollback-config-form.tsx | 257 +++++ .../swarm-forms/stop-grace-period-form.tsx | 152 +++ .../swarm-forms/update-config-form.tsx | 264 +++++ .../advanced/cluster/swarm-forms/utils.ts | 26 + 12 files changed, 2192 insertions(+), 896 deletions(-) create mode 100644 apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/endpoint-spec-form.tsx create mode 100644 apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/health-check-form.tsx create mode 100644 apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/index.ts create mode 100644 apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/labels-form.tsx create mode 100644 apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/mode-form.tsx create mode 100644 apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/placement-form.tsx create mode 100644 apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/restart-policy-form.tsx create mode 100644 apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/rollback-config-form.tsx create mode 100644 apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/stop-grace-period-form.tsx create mode 100644 apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/update-config-form.tsx create mode 100644 apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/utils.ts diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx index 739bd87a5..5721132a7 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx @@ -1,205 +1,73 @@ -import { zodResolver } from "@hookform/resolvers/zod"; -import { HelpCircle, Settings } from "lucide-react"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; +import { Settings } from "lucide-react"; +import { useState } from "react"; import { AlertBlock } from "@/components/shared/alert-block"; -import { CodeEditor } from "@/components/shared/code-editor"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, - DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; +import { cn } from "@/lib/utils"; import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { api } from "@/utils/api"; + EndpointSpecForm, + HealthCheckForm, + LabelsForm, + ModeForm, + PlacementForm, + RestartPolicyForm, + RollbackConfigForm, + StopGracePeriodForm, + UpdateConfigForm, +} from "./swarm-forms"; -const HealthCheckSwarmSchema = z - .object({ - Test: z.array(z.string()).optional(), - Interval: z.number().optional(), - Timeout: z.number().optional(), - StartPeriod: z.number().optional(), - Retries: z.number().optional(), - }) - .strict(); - -const RestartPolicySwarmSchema = z - .object({ - Condition: z.string().optional(), - Delay: z.number().optional(), - MaxAttempts: z.number().optional(), - Window: z.number().optional(), - }) - .strict(); - -const PreferenceSchema = z - .object({ - Spread: z.object({ - SpreadDescriptor: z.string(), - }), - }) - .strict(); - -const PlatformSchema = z - .object({ - Architecture: z.string(), - OS: z.string(), - }) - .strict(); - -const PlacementSwarmSchema = z - .object({ - Constraints: z.array(z.string()).optional(), - Preferences: z.array(PreferenceSchema).optional(), - MaxReplicas: z.number().optional(), - Platforms: z.array(PlatformSchema).optional(), - }) - .strict(); - -const UpdateConfigSwarmSchema = z - .object({ - Parallelism: z.number(), - Delay: z.number().optional(), - FailureAction: z.string().optional(), - Monitor: z.number().optional(), - MaxFailureRatio: z.number().optional(), - Order: z.string(), - }) - .strict(); - -const ReplicatedSchema = z - .object({ - Replicas: z.number().optional(), - }) - .strict(); - -const ReplicatedJobSchema = z - .object({ - MaxConcurrent: z.number().optional(), - TotalCompletions: z.number().optional(), - }) - .strict(); - -const ServiceModeSwarmSchema = z - .object({ - Replicated: ReplicatedSchema.optional(), - Global: z.object({}).optional(), - ReplicatedJob: ReplicatedJobSchema.optional(), - GlobalJob: z.object({}).optional(), - }) - .strict(); - -const NetworkSwarmSchema = z.array( - z - .object({ - Target: z.string().optional(), - Aliases: z.array(z.string()).optional(), - DriverOpts: z.object({}).optional(), - }) - .strict(), -); - -const LabelsSwarmSchema = z.record(z.string()); - -const EndpointPortConfigSwarmSchema = z - .object({ - Protocol: z.string().optional(), - TargetPort: z.number().optional(), - PublishedPort: z.number().optional(), - PublishMode: z.string().optional(), - }) - .strict(); - -const EndpointSpecSwarmSchema = z - .object({ - Mode: z.string().optional(), - Ports: z.array(EndpointPortConfigSwarmSchema).optional(), - }) - .strict(); - -const createStringToJSONSchema = (schema: z.ZodTypeAny) => { - return z - .string() - .transform((str, ctx) => { - if (str === null || str === "") { - return null; - } - try { - return JSON.parse(str); - } catch { - ctx.addIssue({ code: "custom", message: "Invalid JSON format" }); - return z.NEVER; - } - }) - .superRefine((data, ctx) => { - if (data === null) { - return; - } - - if (Object.keys(data).length === 0) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Object cannot be empty", - }); - return; - } - - const parseResult = schema.safeParse(data); - if (!parseResult.success) { - for (const error of parseResult.error.issues) { - const path = error.path.join("."); - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `${path} ${error.message}`, - }); - } - } - }); +type MenuItem = { + id: string; + label: string; + description: string; }; -const addSwarmSettings = z.object({ - healthCheckSwarm: createStringToJSONSchema(HealthCheckSwarmSchema).nullable(), - restartPolicySwarm: createStringToJSONSchema( - RestartPolicySwarmSchema, - ).nullable(), - placementSwarm: createStringToJSONSchema(PlacementSwarmSchema).nullable(), - updateConfigSwarm: createStringToJSONSchema( - UpdateConfigSwarmSchema, - ).nullable(), - rollbackConfigSwarm: createStringToJSONSchema( - UpdateConfigSwarmSchema, - ).nullable(), - modeSwarm: createStringToJSONSchema(ServiceModeSwarmSchema).nullable(), - labelsSwarm: createStringToJSONSchema(LabelsSwarmSchema).nullable(), - networkSwarm: createStringToJSONSchema(NetworkSwarmSchema).nullable(), - stopGracePeriodSwarm: z.bigint().nullable(), - endpointSpecSwarm: createStringToJSONSchema( - EndpointSpecSwarmSchema, - ).nullable(), -}); - -type AddSwarmSettings = z.infer; +const menuItems: MenuItem[] = [ + { + id: "health-check", + label: "Health Check", + description: "Configure health check settings", + }, + { + id: "restart-policy", + label: "Restart Policy", + description: "Configure restart policy", + }, + { + id: "placement", + label: "Placement", + description: "Configure placement constraints", + }, + { + id: "update-config", + label: "Update Config", + description: "Configure update strategy", + }, + { + id: "rollback-config", + label: "Rollback Config", + description: "Configure rollback strategy", + }, + { id: "mode", label: "Mode", description: "Configure service mode" }, + { id: "labels", label: "Labels", description: "Configure service labels" }, + { + id: "stop-grace-period", + label: "Stop Grace Period", + description: "Configure stop grace period", + }, + { + id: "endpoint-spec", + label: "Endpoint Spec", + description: "Configure endpoint specification", + }, +]; const hasStopGracePeriodSwarm = ( value: unknown, @@ -214,137 +82,23 @@ interface Props { } export const AddSwarmSettings = ({ id, type }: Props) => { - const queryMap = { - postgres: () => - api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), - redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), - mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), - mariadb: () => - api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), - application: () => - api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), - mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), - }; - const { data, refetch } = queryMap[type] - ? queryMap[type]() - : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); - - const mutationMap = { - postgres: () => api.postgres.update.useMutation(), - redis: () => api.redis.update.useMutation(), - mysql: () => api.mysql.update.useMutation(), - mariadb: () => api.mariadb.update.useMutation(), - application: () => api.application.update.useMutation(), - mongo: () => api.mongo.update.useMutation(), - }; - - const { mutateAsync, isError, error, isLoading } = mutationMap[type] - ? mutationMap[type]() - : api.mongo.update.useMutation(); - - const form = useForm({ - defaultValues: { - healthCheckSwarm: null, - restartPolicySwarm: null, - placementSwarm: null, - updateConfigSwarm: null, - rollbackConfigSwarm: null, - modeSwarm: null, - labelsSwarm: null, - networkSwarm: null, - stopGracePeriodSwarm: null, - endpointSpecSwarm: null, - }, - resolver: zodResolver(addSwarmSettings), - }); - - useEffect(() => { - if (data) { - const stopGracePeriodValue = hasStopGracePeriodSwarm(data) - ? data.stopGracePeriodSwarm - : null; - const normalizedStopGracePeriod = - stopGracePeriodValue === null || stopGracePeriodValue === undefined - ? null - : typeof stopGracePeriodValue === "bigint" - ? stopGracePeriodValue - : BigInt(stopGracePeriodValue); - form.reset({ - healthCheckSwarm: data.healthCheckSwarm - ? JSON.stringify(data.healthCheckSwarm, null, 2) - : null, - restartPolicySwarm: data.restartPolicySwarm - ? JSON.stringify(data.restartPolicySwarm, null, 2) - : null, - placementSwarm: data.placementSwarm - ? JSON.stringify(data.placementSwarm, null, 2) - : null, - updateConfigSwarm: data.updateConfigSwarm - ? JSON.stringify(data.updateConfigSwarm, null, 2) - : null, - rollbackConfigSwarm: data.rollbackConfigSwarm - ? JSON.stringify(data.rollbackConfigSwarm, null, 2) - : null, - modeSwarm: data.modeSwarm - ? JSON.stringify(data.modeSwarm, null, 2) - : null, - labelsSwarm: data.labelsSwarm - ? JSON.stringify(data.labelsSwarm, null, 2) - : null, - networkSwarm: data.networkSwarm - ? JSON.stringify(data.networkSwarm, null, 2) - : null, - stopGracePeriodSwarm: normalizedStopGracePeriod, - endpointSpecSwarm: data.endpointSpecSwarm - ? JSON.stringify(data.endpointSpecSwarm, null, 2) - : null, - }); - } - }, [form, form.reset, data]); - - const onSubmit = async (data: AddSwarmSettings) => { - await mutateAsync({ - applicationId: id || "", - postgresId: id || "", - redisId: id || "", - mysqlId: id || "", - mariadbId: id || "", - mongoId: id || "", - healthCheckSwarm: data.healthCheckSwarm, - restartPolicySwarm: data.restartPolicySwarm, - placementSwarm: data.placementSwarm, - updateConfigSwarm: data.updateConfigSwarm, - rollbackConfigSwarm: data.rollbackConfigSwarm, - modeSwarm: data.modeSwarm, - labelsSwarm: data.labelsSwarm, - networkSwarm: data.networkSwarm, - stopGracePeriodSwarm: data.stopGracePeriodSwarm ?? null, - endpointSpecSwarm: data.endpointSpecSwarm, - }) - .then(async () => { - toast.success("Swarm settings updated"); - refetch(); - }) - .catch(() => { - toast.error("Error updating the swarm settings"); - }); - }; + const [activeMenu, setActiveMenu] = useState("health-check"); + const [open, setOpen] = useState(false); return ( - + - + Swarm Settings - Update certain settings using a json object. + Configure swarm settings for your service. - {isError && {error?.message}}
Changing settings such as placements may cause the logs/monitoring, @@ -352,596 +106,56 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
-
- - ( - - Health Check - - - - - Check the interface - - - - - -
-														{`{
-	Test?: string[] | undefined;
-	Interval?: number | undefined;
-	Timeout?: number | undefined;
-	StartPeriod?: number | undefined;
-	Retries?: number | undefined;
-}`}
-													
-
-
-
-
+
+ {/* Left Column - Menu */} +
+ +
- - - -
-										
-									
- - )} - /> - - ( - - Restart Policy - - - - - Check the interface - - - - - -
-														{`{
-	Condition?: string | undefined;
-	Delay?: number | undefined;
-	MaxAttempts?: number | undefined;
-	Window?: number | undefined;
-}`}
-													
-
-
-
-
- - - - -
-										
-									
-
- )} - /> - - ( - - Placement - - - - - Check the interface - - - - - -
-														{`{
-	Constraints?: string[] | undefined;
-	Preferences?: Array<{ Spread: { SpreadDescriptor: string } }> | undefined;
-	MaxReplicas?: number | undefined;
-	Platforms?:
-		| Array<{
-				Architecture: string;
-				OS: string;
-		  }>
-		| undefined;
-}`}
-													
-
-
-
-
- - - - -
-										
-									
-
- )} - /> - - ( - - Update Config - - - - - Check the interface - - - - - -
-														{`{
-	Parallelism?: number;
-	Delay?: number | undefined;
-	FailureAction?: string | undefined;
-	Monitor?: number | undefined;
-	MaxFailureRatio?: number | undefined;
-	Order: string;
-}`}
-													
-
-
-
-
- - - - -
-										
-									
-
- )} - /> - - ( - - Rollback Config - - - - - Check the interface - - - - - -
-														{`{
-	Parallelism?: number;
-	Delay?: number | undefined;
-	FailureAction?: string | undefined;
-	Monitor?: number | undefined;
-	MaxFailureRatio?: number | undefined;
-	Order: string;
-}`}
-													
-
-
-
-
- - - - -
-										
-									
-
- )} - /> - - ( - - Mode - - - - - Check the interface - - - - - -
-														{`{
-	Replicated?: { Replicas?: number | undefined } | undefined;
-	Global?: {} | undefined;
-	ReplicatedJob?:
-		| {
-				MaxConcurrent?: number | undefined;
-				TotalCompletions?: number | undefined;
-		  }
-		| undefined;
-	GlobalJob?: {} | undefined;
-}`}
-													
-
-
-
-
- - - - -
-										
-									
-
- )} - /> - ( - - Network - - - - - Check the interface - - - - - -
-														{`[
-  {
-	"Target" : string | undefined;
-	"Aliases" : string[] | undefined;
-	"DriverOpts" : { [key: string]: string } | undefined;
-  }
-]`}
-													
-
-
-
-
- - - -
-										
-									
-
- )} - /> - ( - - Labels - - - - - Check the interface - - - - - -
-														{`{
-	[name: string]: string;
-}`}
-													
-
-
-
-
- - - -
-										
-									
-
- )} - /> - ( - - Stop Grace Period (nanoseconds) - - - - - Duration in nanoseconds - - - - - -
-														{`Enter duration in nanoseconds:
-														• 30000000000 - 30 seconds
-														• 120000000000 - 2 minutes  
-														• 3600000000000 - 1 hour
-														• 0 - no grace period`}
-													
-
-
-
-
- - - field.onChange( - e.target.value ? BigInt(e.target.value) : null, - ) - } - /> - -
-										
-									
-
- )} - /> - ( - - Endpoint Spec - - - - - Check the interface - - - - - -
-														{`{
-	Mode?: string | undefined;
-	Ports?: Array<{
-		Protocol?: string | undefined;
-		TargetPort?: number | undefined;
-		PublishedPort?: number | undefined;
-		PublishMode?: string | undefined;
-	}> | undefined;
-}`}
-													
-
-
-
-
- - - - -
-										
-									
-
- )} - /> - - - - - + {/* Right Column - Form */} +
+ {activeMenu === "health-check" && ( + + )} + {activeMenu === "restart-policy" && ( + + )} + {activeMenu === "placement" && ( + + )} + {activeMenu === "update-config" && ( + + )} + {activeMenu === "rollback-config" && ( + + )} + {activeMenu === "mode" && } + {activeMenu === "labels" && } + {activeMenu === "stop-grace-period" && ( + + )} + {activeMenu === "endpoint-spec" && ( + + )} +
+
); diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/endpoint-spec-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/endpoint-spec-form.tsx new file mode 100644 index 000000000..9bd6735c3 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/endpoint-spec-form.tsx @@ -0,0 +1,151 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { api } from "@/utils/api"; + +export const endpointSpecFormSchema = z.object({ + Mode: z.string().optional(), +}); + +interface EndpointSpecFormProps { + id: string; + type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; +} + +export const EndpointSpecForm = ({ id, type }: EndpointSpecFormProps) => { + const [isLoading, setIsLoading] = useState(false); + + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; + + const { mutateAsync } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); + + const form = useForm({ + resolver: zodResolver(endpointSpecFormSchema), + defaultValues: { + Mode: undefined, + }, + }); + + useEffect(() => { + if (data?.endpointSpecSwarm) { + const es = data.endpointSpecSwarm; + form.reset({ + Mode: es.Mode, + }); + } + }, [data, form]); + + const onSubmit = async (formData: z.infer) => { + setIsLoading(true); + try { + // Check if all values are empty, if so, send null to clear the database + const hasAnyValue = formData.Mode !== undefined && formData.Mode !== null && formData.Mode !== ""; + + await mutateAsync({ + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + endpointSpecSwarm: hasAnyValue ? formData : null, + }); + + toast.success("Endpoint spec updated successfully"); + refetch(); + } catch { + toast.error("Error updating endpoint spec"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + ( + + Mode + Endpoint mode (vip or dnsrr) + + + + )} + /> + +
+ + +
+ + + ); +}; diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/health-check-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/health-check-form.tsx new file mode 100644 index 000000000..378be5dbb --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/health-check-form.tsx @@ -0,0 +1,267 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { api } from "@/utils/api"; + +export const healthCheckFormSchema = z.object({ + Test: z.array(z.string()).optional(), + Interval: z.coerce.number().optional(), + Timeout: z.coerce.number().optional(), + StartPeriod: z.coerce.number().optional(), + Retries: z.coerce.number().optional(), +}); + +interface HealthCheckFormProps { + id: string; + type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; +} + +export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => { + const [isLoading, setIsLoading] = useState(false); + const [testCommands, setTestCommands] = useState([]); + + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; + + const { mutateAsync } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); + + const form = useForm({ + resolver: zodResolver(healthCheckFormSchema), + defaultValues: { + Test: [], + Interval: undefined, + Timeout: undefined, + StartPeriod: undefined, + Retries: undefined, + }, + }); + + useEffect(() => { + if (data?.healthCheckSwarm) { + const hc = data.healthCheckSwarm; + form.reset({ + Test: hc.Test || [], + Interval: hc.Interval, + Timeout: hc.Timeout, + StartPeriod: hc.StartPeriod, + Retries: hc.Retries, + }); + setTestCommands(hc.Test || []); + } + }, [data, form]); + + const onSubmit = async (formData: z.infer) => { + setIsLoading(true); + try { + // Check if all values are empty, if so, send null to clear the database + const hasAnyValue = + (formData.Test && formData.Test.length > 0) || + formData.Interval !== undefined || + formData.Timeout !== undefined || + formData.StartPeriod !== undefined || + formData.Retries !== undefined; + + await mutateAsync({ + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + healthCheckSwarm: hasAnyValue ? formData : null, + }); + + toast.success("Health check updated successfully"); + refetch(); + } catch { + toast.error("Error updating health check"); + } finally { + setIsLoading(false); + } + }; + + const addTestCommand = () => { + setTestCommands([...testCommands, ""]); + }; + + const updateTestCommand = (index: number, value: string) => { + const newCommands = [...testCommands]; + newCommands[index] = value; + setTestCommands(newCommands); + }; + + const removeTestCommand = (index: number) => { + setTestCommands(testCommands.filter((_, i) => i !== index)); + }; + + return ( +
+ +
+ Test Commands + + Command to run for health check (e.g., ["CMD-SHELL", "curl -f + http://localhost:3000/health"]) + +
+ {testCommands.map((cmd, index) => ( +
+ updateTestCommand(index, e.target.value)} + placeholder={ + index === 0 + ? "CMD-SHELL" + : "curl -f http://localhost:3000/health" + } + /> + +
+ ))} + +
+
+ + ( + + Interval (nanoseconds) + + Time between health checks (e.g., 10000000000 for 10 seconds) + + + + + + + )} + /> + + ( + + Timeout (nanoseconds) + + Maximum time to wait for health check response + + + + + + + )} + /> + + ( + + Start Period (nanoseconds) + + Initial grace period before health checks begin + + + + + + + )} + /> + + ( + + Retries + + Number of consecutive failures needed to consider container + unhealthy + + + + + + + )} + /> + +
+ + +
+ + + ); +}; diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/index.ts b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/index.ts new file mode 100644 index 000000000..ebd00abcd --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/index.ts @@ -0,0 +1,10 @@ +export { HealthCheckForm } from "./health-check-form"; +export { RestartPolicyForm } from "./restart-policy-form"; +export { PlacementForm } from "./placement-form"; +export { UpdateConfigForm } from "./update-config-form"; +export { RollbackConfigForm } from "./rollback-config-form"; +export { ModeForm } from "./mode-form"; +export { LabelsForm } from "./labels-form"; +export { StopGracePeriodForm } from "./stop-grace-period-form"; +export { EndpointSpecForm } from "./endpoint-spec-form"; +export { filterEmptyValues, hasValues } from "./utils"; diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/labels-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/labels-form.tsx new file mode 100644 index 000000000..db173313b --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/labels-form.tsx @@ -0,0 +1,199 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { useFieldArray, useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { api } from "@/utils/api"; + +export const labelsFormSchema = z.object({ + labels: z + .array( + z.object({ + key: z.string(), + value: z.string(), + }), + ) + .optional(), +}); + +interface LabelsFormProps { + id: string; + type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; +} + +export const LabelsForm = ({ id, type }: LabelsFormProps) => { + const [isLoading, setIsLoading] = useState(false); + + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; + + const { mutateAsync } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); + + const form = useForm({ + resolver: zodResolver(labelsFormSchema), + defaultValues: { + labels: [], + }, + }); + + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: "labels", + }); + + useEffect(() => { + if (data?.labelsSwarm && typeof data.labelsSwarm === "object") { + const labelEntries = Object.entries(data.labelsSwarm).map( + ([key, value]) => ({ + key, + value: value as string, + }), + ); + form.reset({ labels: labelEntries }); + } + }, [data, form]); + + const onSubmit = async (formData: z.infer) => { + setIsLoading(true); + try { + const labelsObject = + formData.labels?.reduce( + (acc, { key, value }) => { + if (key && value) { + acc[key] = value; + } + return acc; + }, + {} as Record, + ) || {}; + + // If no labels, send null to clear the database + const labelsToSend = Object.keys(labelsObject).length > 0 ? labelsObject : null; + + await mutateAsync({ + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + labelsSwarm: labelsToSend, + }); + + toast.success("Labels updated successfully"); + refetch(); + } catch { + toast.error("Error updating labels"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ +
+ Labels + + Add key-value labels to your service + +
+ {fields.map((field, index) => ( +
+ ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + +
+ ))} + +
+
+ +
+ + +
+
+ + ); +}; diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/mode-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/mode-form.tsx new file mode 100644 index 000000000..839f5d519 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/mode-form.tsx @@ -0,0 +1,195 @@ +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { api } from "@/utils/api"; + +interface ModeFormProps { + id: string; + type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; +} + +export const ModeForm = ({ id, type }: ModeFormProps) => { + const [isLoading, setIsLoading] = useState(false); + + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; + + const { mutateAsync } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); + + const form = useForm({ + defaultValues: { + type: undefined, + Replicas: undefined, + }, + }); + + const modeType = form.watch("type"); + + useEffect(() => { + if (data?.modeSwarm) { + const mode = data.modeSwarm; + if (mode.Replicated) { + form.reset({ + type: "Replicated", + Replicas: mode.Replicated.Replicas, + }); + } else if (mode.Global) { + form.reset({ + type: "Global", + Replicas: undefined, + }); + } + } + }, [data, form]); + + const onSubmit = async (formData: any) => { + setIsLoading(true); + try { + // If no type is selected, send null to clear the database + if (!formData.type) { + await mutateAsync({ + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + modeSwarm: null, + }); + toast.success("Mode updated successfully"); + refetch(); + setIsLoading(false); + return; + } + + const modeData = + formData.type === "Replicated" + ? { Replicated: { Replicas: formData.Replicas } } + : { Global: {} }; + + await mutateAsync({ + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + modeSwarm: modeData, + }); + + toast.success("Mode updated successfully"); + refetch(); + } catch { + toast.error("Error updating mode"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + ( + + Mode Type + + Choose between replicated or global service mode + + + + + )} + /> + + {modeType === "Replicated" && ( + ( + + Replicas + Number of replicas to run + + + + + + )} + /> + )} + +
+ + +
+ + + ); +}; diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/placement-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/placement-form.tsx new file mode 100644 index 000000000..7c2ef074c --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/placement-form.tsx @@ -0,0 +1,342 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { api } from "@/utils/api"; + +const PreferenceSchema = z.object({ + Spread: z.object({ + SpreadDescriptor: z.string(), + }), +}); + +const PlatformSchema = z.object({ + Architecture: z.string(), + OS: z.string(), +}); + +export const placementFormSchema = z.object({ + Constraints: z.array(z.string()).optional(), + Preferences: z.array(PreferenceSchema).optional(), + MaxReplicas: z.coerce.number().optional(), + Platforms: z.array(PlatformSchema).optional(), +}); + +interface PlacementFormProps { + id: string; + type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; +} + +export const PlacementForm = ({ id, type }: PlacementFormProps) => { + const [isLoading, setIsLoading] = useState(false); + + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; + + const { mutateAsync } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); + + const form = useForm({ + resolver: zodResolver(placementFormSchema), + defaultValues: { + Constraints: [], + Preferences: [], + MaxReplicas: undefined, + Platforms: [], + }, + }); + + const constraints = form.watch("Constraints") || []; + const preferences = form.watch("Preferences") || []; + const platforms = form.watch("Platforms") || []; + + useEffect(() => { + if (data?.placementSwarm) { + const placement = data.placementSwarm; + form.reset({ + Constraints: placement.Constraints || [], + Preferences: + placement.Preferences?.map((p: any) => ({ + SpreadDescriptor: p.Spread?.SpreadDescriptor || "", + })) || [], + MaxReplicas: placement.MaxReplicas, + Platforms: placement.Platforms || [], + }); + } + }, [data, form]); + + const onSubmit = async (formData: z.infer) => { + setIsLoading(true); + try { + // Check if all values are empty, if so, send null to clear the database + const hasAnyValue = + (formData.Constraints && formData.Constraints.length > 0) || + (formData.Preferences && formData.Preferences.length > 0) || + (formData.Platforms && formData.Platforms.length > 0) || + formData.MaxReplicas !== undefined; + + await mutateAsync({ + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + placementSwarm: hasAnyValue ? formData : null, + }); + + toast.success("Placement updated successfully"); + refetch(); + } catch { + toast.error("Error updating placement"); + } finally { + setIsLoading(false); + } + }; + + const addConstraint = () => { + form.setValue("Constraints", [...constraints, ""]); + }; + + const updateConstraint = (index: number, value: string) => { + const newConstraints = [...constraints]; + newConstraints[index] = value; + form.setValue("Constraints", newConstraints); + }; + + const removeConstraint = (index: number) => { + form.setValue( + "Constraints", + constraints.filter((_: string, i: number) => i !== index), + ); + }; + + const addPreference = () => { + form.setValue("Preferences", [...preferences, { SpreadDescriptor: "" }]); + }; + + const updatePreference = (index: number, value: string) => { + const newPreferences = [...preferences]; + if (newPreferences[index]) { + newPreferences[index].SpreadDescriptor = value; + form.setValue("Preferences", newPreferences); + } + }; + + const removePreference = (index: number) => { + form.setValue( + "Preferences", + preferences.filter((_: any, i: number) => i !== index), + ); + }; + + const addPlatform = () => { + form.setValue("Platforms", [...platforms, { Architecture: "", OS: "" }]); + }; + + const updatePlatform = ( + index: number, + field: "Architecture" | "OS", + value: string, + ) => { + const newPlatforms = [...platforms]; + if (newPlatforms[index]) { + newPlatforms[index][field] = value; + form.setValue("Platforms", newPlatforms); + } + }; + + const removePlatform = (index: number) => { + form.setValue( + "Platforms", + platforms.filter((_: any, i: number) => i !== index), + ); + }; + + return ( +
+ +
+ Constraints + + Placement constraints (e.g., "node.role==manager") + +
+ {constraints.map((constraint: string, index: number) => ( +
+ updateConstraint(index, e.target.value)} + placeholder="node.role==manager" + /> + +
+ ))} + +
+
+ +
+ Preferences + + Spread preferences for task distribution (e.g., + "node.labels.region") + +
+ {preferences.map((pref: any, index: number) => ( +
+ updatePreference(index, e.target.value)} + placeholder="node.labels.region" + /> + +
+ ))} + +
+
+ + ( + + Max Replicas + + Maximum number of replicas per node + + + + + + + )} + /> + +
+ Platforms + + Target platforms for task scheduling + +
+ {platforms.map((platform: any, index: number) => ( +
+ + updatePlatform(index, "Architecture", e.target.value) + } + placeholder="amd64" + /> + updatePlatform(index, "OS", e.target.value)} + placeholder="linux" + /> + +
+ ))} + +
+
+ +
+ + +
+ + + ); +}; diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/restart-policy-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/restart-policy-form.tsx new file mode 100644 index 000000000..395855231 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/restart-policy-form.tsx @@ -0,0 +1,219 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { api } from "@/utils/api"; + +export const restartPolicyFormSchema = z.object({ + Condition: z.string().optional(), + Delay: z.coerce.number().optional(), + MaxAttempts: z.coerce.number().optional(), + Window: z.coerce.number().optional(), +}); + +interface RestartPolicyFormProps { + id: string; + type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; +} + +export const RestartPolicyForm = ({ id, type }: RestartPolicyFormProps) => { + const [isLoading, setIsLoading] = useState(false); + + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; + + const { mutateAsync } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); + + const form = useForm({ + resolver: zodResolver(restartPolicyFormSchema), + defaultValues: { + Condition: undefined, + Delay: undefined, + MaxAttempts: undefined, + Window: undefined, + }, + }); + + useEffect(() => { + if (data?.restartPolicySwarm) { + form.reset({ + Condition: data.restartPolicySwarm.Condition, + Delay: data.restartPolicySwarm.Delay, + MaxAttempts: data.restartPolicySwarm.MaxAttempts, + Window: data.restartPolicySwarm.Window, + }); + } + }, [data, form]); + + const onSubmit = async ( + formData: z.infer, + ) => { + setIsLoading(true); + try { + // Check if all values are empty, if so, send null to clear the database + const hasAnyValue = Object.values(formData).some( + value => value !== undefined && value !== null && value !== "" + ); + + await mutateAsync({ + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + restartPolicySwarm: hasAnyValue ? formData : null, + }); + + toast.success("Restart policy updated successfully"); + refetch(); + } catch { + toast.error("Error updating restart policy"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + ( + + Condition + When to restart the container + + + + )} + /> + + ( + + Delay (nanoseconds) + + Wait time between restart attempts + + + + + + + )} + /> + + ( + + Max Attempts + + Maximum number of restart attempts + + + + + + + )} + /> + + ( + + Window (nanoseconds) + + Time window to evaluate restart policy + + + + + + + )} + /> + +
+ + +
+ + + ); +}; diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/rollback-config-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/rollback-config-form.tsx new file mode 100644 index 000000000..3f298a7e8 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/rollback-config-form.tsx @@ -0,0 +1,257 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { api } from "@/utils/api"; + +export const rollbackConfigFormSchema = z.object({ + Parallelism: z.coerce.number().optional(), + Delay: z.coerce.number().optional(), + FailureAction: z.string().optional(), + Monitor: z.coerce.number().optional(), + MaxFailureRatio: z.coerce.number().optional(), + Order: z.string().optional(), +}); + +interface RollbackConfigFormProps { + id: string; + type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; +} + +export const RollbackConfigForm = ({ id, type }: RollbackConfigFormProps) => { + const [isLoading, setIsLoading] = useState(false); + + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; + + const { mutateAsync } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); + + const form = useForm({ + resolver: zodResolver(rollbackConfigFormSchema), + defaultValues: { + Parallelism: undefined, + Delay: undefined, + FailureAction: undefined, + Monitor: undefined, + MaxFailureRatio: undefined, + Order: undefined, + }, + }); + + useEffect(() => { + if (data?.rollbackConfigSwarm) { + form.reset(data.rollbackConfigSwarm); + } + }, [data, form]); + + const onSubmit = async ( + formData: z.infer, + ) => { + setIsLoading(true); + try { + // Check if all values are empty, if so, send null to clear the database + const hasAnyValue = Object.values(formData).some( + value => value !== undefined && value !== null && value !== "" + ); + + await mutateAsync({ + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + rollbackConfigSwarm: hasAnyValue ? formData : null, + }); + + toast.success("Rollback config updated successfully"); + refetch(); + } catch { + toast.error("Error updating rollback config"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + ( + + Parallelism + + Number of tasks to rollback simultaneously + + + + + + + )} + /> + + ( + + Delay (nanoseconds) + Delay between task rollbacks + + + + + + )} + /> + + ( + + Failure Action + Action on rollback failure + + + + )} + /> + + ( + + Monitor (nanoseconds) + + Duration to monitor for failure after rollback + + + + + + + )} + /> + + ( + + Max Failure Ratio + + Maximum failure ratio tolerated (0-1) + + + + + + + )} + /> + + ( + + Order + Rollback order strategy + + + + )} + /> + +
+ + +
+ + + ); +}; diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/stop-grace-period-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/stop-grace-period-form.tsx new file mode 100644 index 000000000..30194557b --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/stop-grace-period-form.tsx @@ -0,0 +1,152 @@ +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { api } from "@/utils/api"; + +const hasStopGracePeriodSwarm = ( + value: unknown, +): value is { stopGracePeriodSwarm: bigint | number | string | null } => + typeof value === "object" && + value !== null && + "stopGracePeriodSwarm" in value; + +interface StopGracePeriodFormProps { + id: string; + type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; +} + +export const StopGracePeriodForm = ({ id, type }: StopGracePeriodFormProps) => { + const [isLoading, setIsLoading] = useState(false); + + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; + + const { mutateAsync } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); + + const form = useForm({ + defaultValues: { + value: null as bigint | null, + }, + }); + + useEffect(() => { + if (hasStopGracePeriodSwarm(data)) { + const value = data.stopGracePeriodSwarm; + const normalizedValue = + value === null || value === undefined + ? null + : typeof value === "bigint" + ? value + : BigInt(value); + form.reset({ + value: normalizedValue, + }); + } + }, [data, form]); + + const onSubmit = async (formData: any) => { + setIsLoading(true); + try { + await mutateAsync({ + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + stopGracePeriodSwarm: formData.value, + }); + + toast.success("Stop grace period updated successfully"); + refetch(); + } catch { + toast.error("Error updating stop grace period"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + ( + + Stop Grace Period (nanoseconds) + + Time to wait before forcefully killing the container +
+ Examples: 30000000000 (30s), 120000000000 (2m) +
+ + + field.onChange(e.target.value ? BigInt(e.target.value) : null) + } + /> + + +
+ )} + /> + +
+ + +
+ + + ); +}; diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/update-config-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/update-config-form.tsx new file mode 100644 index 000000000..f756e47e1 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/update-config-form.tsx @@ -0,0 +1,264 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { api } from "@/utils/api"; + +export const updateConfigFormSchema = z.object({ + Parallelism: z.coerce.number().optional(), + Delay: z.coerce.number().optional(), + FailureAction: z.string().optional(), + Monitor: z.coerce.number().optional(), + MaxFailureRatio: z.coerce.number().optional(), + Order: z.string().optional(), +}); + +interface UpdateConfigFormProps { + id: string; + type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; +} + +export const UpdateConfigForm = ({ id, type }: UpdateConfigFormProps) => { + const [isLoading, setIsLoading] = useState(false); + + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; + + const { mutateAsync } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); + + const form = useForm({ + resolver: zodResolver(updateConfigFormSchema), + defaultValues: { + Parallelism: undefined, + Delay: undefined, + FailureAction: undefined, + Monitor: undefined, + MaxFailureRatio: undefined, + Order: undefined, + }, + }); + + useEffect(() => { + if (data?.updateConfigSwarm) { + const config = data.updateConfigSwarm; + form.reset({ + Parallelism: config.Parallelism, + Delay: config.Delay, + FailureAction: config.FailureAction, + Monitor: config.Monitor, + MaxFailureRatio: config.MaxFailureRatio, + Order: config.Order, + }); + } + }, [data, form]); + + const onSubmit = async (formData: z.infer) => { + setIsLoading(true); + try { + // Check if all values are empty, if so, send null to clear the database + const hasAnyValue = Object.values(formData).some( + value => value !== undefined && value !== null && value !== "" + ); + + await mutateAsync({ + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + updateConfigSwarm: hasAnyValue ? formData : null, + }); + + toast.success("Update config updated successfully"); + refetch(); + } catch { + toast.error("Error updating update config"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + ( + + Parallelism + + Number of tasks to update simultaneously + + + + + + + )} + /> + + ( + + Delay (nanoseconds) + Delay between task updates + + + + + + )} + /> + + ( + + Failure Action + Action on update failure + + + + )} + /> + + ( + + Monitor (nanoseconds) + + Duration to monitor for failure after update + + + + + + + )} + /> + + ( + + Max Failure Ratio + + Maximum failure ratio tolerated (0-1) + + + + + + + )} + /> + + ( + + Order + Update order strategy + + + + )} + /> + +
+ + +
+ + + ); +}; diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/utils.ts b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/utils.ts new file mode 100644 index 000000000..b6120ec9b --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/utils.ts @@ -0,0 +1,26 @@ +/** + * Filters out undefined, null, and empty string values from form data + * Only returns fields that have actual values + */ +export const filterEmptyValues = (formData: Record): Record => { + return Object.entries(formData).reduce((acc, [key, value]) => { + // Keep arrays even if empty (they might be intentionally cleared) + if (Array.isArray(value)) { + if (value.length > 0) { + acc[key] = value; + } + } + // For other values, filter out undefined, null, and empty strings + else if (value !== undefined && value !== null && value !== "") { + acc[key] = value; + } + return acc; + }, {} as Record); +}; + +/** + * Checks if filtered data has any values to save + */ +export const hasValues = (data: Record): boolean => { + return Object.keys(data).length > 0; +}; From a0d8eb9380d613b199191d84856e39f0e1a29380 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 20 Jan 2026 16:02:11 +0100 Subject: [PATCH 15/40] fix(labels-form): improve readability of labelsToSend assignment --- .../application/advanced/cluster/swarm-forms/labels-form.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/labels-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/labels-form.tsx index db173313b..d1681dcd0 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/labels-form.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/labels-form.tsx @@ -102,7 +102,8 @@ export const LabelsForm = ({ id, type }: LabelsFormProps) => { ) || {}; // If no labels, send null to clear the database - const labelsToSend = Object.keys(labelsObject).length > 0 ? labelsObject : null; + const labelsToSend = + Object.keys(labelsObject).length > 0 ? labelsObject : null; await mutateAsync({ applicationId: id || "", From 7e48b2cf29687a4e2ec4e9281eeec6830adab598 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:02:58 +0000 Subject: [PATCH 16/40] [autofix.ci] apply automated fixes --- .../swarm-forms/endpoint-spec-form.tsx | 5 +++- .../cluster/swarm-forms/health-check-form.tsx | 2 +- .../cluster/swarm-forms/placement-form.tsx | 2 +- .../swarm-forms/restart-policy-form.tsx | 2 +- .../swarm-forms/rollback-config-form.tsx | 2 +- .../swarm-forms/stop-grace-period-form.tsx | 10 +++++-- .../swarm-forms/update-config-form.tsx | 2 +- .../advanced/cluster/swarm-forms/utils.ts | 29 +++++++++++-------- 8 files changed, 34 insertions(+), 20 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/endpoint-spec-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/endpoint-spec-form.tsx index 9bd6735c3..7ee31e5b6 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/endpoint-spec-form.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/endpoint-spec-form.tsx @@ -82,7 +82,10 @@ export const EndpointSpecForm = ({ id, type }: EndpointSpecFormProps) => { setIsLoading(true); try { // Check if all values are empty, if so, send null to clear the database - const hasAnyValue = formData.Mode !== undefined && formData.Mode !== null && formData.Mode !== ""; + const hasAnyValue = + formData.Mode !== undefined && + formData.Mode !== null && + formData.Mode !== ""; await mutateAsync({ applicationId: id || "", diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/health-check-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/health-check-form.tsx index 378be5dbb..b2fc49ef3 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/health-check-form.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/health-check-form.tsx @@ -90,7 +90,7 @@ export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => { setIsLoading(true); try { // Check if all values are empty, if so, send null to clear the database - const hasAnyValue = + const hasAnyValue = (formData.Test && formData.Test.length > 0) || formData.Interval !== undefined || formData.Timeout !== undefined || diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/placement-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/placement-form.tsx index 7c2ef074c..b0c354513 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/placement-form.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/placement-form.tsx @@ -103,7 +103,7 @@ export const PlacementForm = ({ id, type }: PlacementFormProps) => { setIsLoading(true); try { // Check if all values are empty, if so, send null to clear the database - const hasAnyValue = + const hasAnyValue = (formData.Constraints && formData.Constraints.length > 0) || (formData.Preferences && formData.Preferences.length > 0) || (formData.Platforms && formData.Platforms.length > 0) || diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/restart-policy-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/restart-policy-form.tsx index 395855231..b7fb649be 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/restart-policy-form.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/restart-policy-form.tsx @@ -94,7 +94,7 @@ export const RestartPolicyForm = ({ id, type }: RestartPolicyFormProps) => { try { // Check if all values are empty, if so, send null to clear the database const hasAnyValue = Object.values(formData).some( - value => value !== undefined && value !== null && value !== "" + (value) => value !== undefined && value !== null && value !== "", ); await mutateAsync({ diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/rollback-config-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/rollback-config-form.tsx index 3f298a7e8..c9c6ad128 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/rollback-config-form.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/rollback-config-form.tsx @@ -93,7 +93,7 @@ export const RollbackConfigForm = ({ id, type }: RollbackConfigFormProps) => { try { // Check if all values are empty, if so, send null to clear the database const hasAnyValue = Object.values(formData).some( - value => value !== undefined && value !== null && value !== "" + (value) => value !== undefined && value !== null && value !== "", ); await mutateAsync({ diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/stop-grace-period-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/stop-grace-period-form.tsx index 30194557b..a324da31b 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/stop-grace-period-form.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/stop-grace-period-form.tsx @@ -119,9 +119,15 @@ export const StopGracePeriodForm = ({ id, type }: StopGracePeriodFormProps) => { type="number" placeholder="30000000000" {...field} - value={field?.value !== null && field?.value !== undefined ? field.value.toString() : ""} + value={ + field?.value !== null && field?.value !== undefined + ? field.value.toString() + : "" + } onChange={(e) => - field.onChange(e.target.value ? BigInt(e.target.value) : null) + field.onChange( + e.target.value ? BigInt(e.target.value) : null, + ) } /> diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/update-config-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/update-config-form.tsx index f756e47e1..26c42adff 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/update-config-form.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/update-config-form.tsx @@ -99,7 +99,7 @@ export const UpdateConfigForm = ({ id, type }: UpdateConfigFormProps) => { try { // Check if all values are empty, if so, send null to clear the database const hasAnyValue = Object.values(formData).some( - value => value !== undefined && value !== null && value !== "" + (value) => value !== undefined && value !== null && value !== "", ); await mutateAsync({ diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/utils.ts b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/utils.ts index b6120ec9b..58793c02e 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/utils.ts +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/utils.ts @@ -2,20 +2,25 @@ * Filters out undefined, null, and empty string values from form data * Only returns fields that have actual values */ -export const filterEmptyValues = (formData: Record): Record => { - return Object.entries(formData).reduce((acc, [key, value]) => { - // Keep arrays even if empty (they might be intentionally cleared) - if (Array.isArray(value)) { - if (value.length > 0) { +export const filterEmptyValues = ( + formData: Record, +): Record => { + return Object.entries(formData).reduce( + (acc, [key, value]) => { + // Keep arrays even if empty (they might be intentionally cleared) + if (Array.isArray(value)) { + if (value.length > 0) { + acc[key] = value; + } + } + // For other values, filter out undefined, null, and empty strings + else if (value !== undefined && value !== null && value !== "") { acc[key] = value; } - } - // For other values, filter out undefined, null, and empty strings - else if (value !== undefined && value !== null && value !== "") { - acc[key] = value; - } - return acc; - }, {} as Record); + return acc; + }, + {} as Record, + ); }; /** From a76147d820e53236c370e5e750c9511e6acb4dea Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 20 Jan 2026 16:19:12 +0100 Subject: [PATCH 17/40] feat(cluster): enhance swarm settings UI with tooltips and documentation links - Added tooltips to menu items in the swarm settings for better user guidance. - Included documentation URLs and descriptions for Health Check, Restart Policy, Placement, Update Config, Rollback Config, Mode, Labels, Stop Grace Period, and Endpoint Spec. - Updated type assertions in rollback and update config forms for improved type safety. --- .../cluster/modify-swarm-settings.tsx | 115 +++++++++++++++--- .../swarm-forms/rollback-config-form.tsx | 2 +- .../swarm-forms/update-config-form.tsx | 2 +- 3 files changed, 98 insertions(+), 21 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx index 5721132a7..aa96d320d 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx @@ -1,4 +1,4 @@ -import { Settings } from "lucide-react"; +import { ExternalLink, Settings } from "lucide-react"; import { useState } from "react"; import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; @@ -10,6 +10,12 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { EndpointSpecForm, @@ -27,6 +33,8 @@ type MenuItem = { id: string; label: string; description: string; + docUrl?: string; + docDescription?: string; }; const menuItems: MenuItem[] = [ @@ -34,38 +42,81 @@ const menuItems: MenuItem[] = [ id: "health-check", label: "Health Check", description: "Configure health check settings", + docUrl: + "https://docs.docker.com/reference/cli/docker/service/create/#healthcheck", + docDescription: + "Configure HEALTHCHECK to test a container's health. Determines if a container is healthy by running a command inside the container.", }, { id: "restart-policy", label: "Restart Policy", description: "Configure restart policy", + docUrl: + "https://docs.docker.com/reference/cli/docker/service/create/#restart-policy", + docDescription: + "Configure the restart policy for containers in the service. Controls when and how containers should be restarted.", }, { id: "placement", label: "Placement", description: "Configure placement constraints", + docUrl: + "https://docs.docker.com/reference/cli/docker/service/create/#placement-pref", + docDescription: + "Control which nodes service tasks can be scheduled on. Use constraints, preferences, and platform specifications.", }, { id: "update-config", label: "Update Config", description: "Configure update strategy", + docUrl: + "https://docs.docker.com/reference/cli/docker/service/create/#update-config", + docDescription: + "Configure how the service should be updated. Controls parallelism, delay, failure action, and order of updates.", }, { id: "rollback-config", label: "Rollback Config", description: "Configure rollback strategy", + docUrl: + "https://docs.docker.com/reference/cli/docker/service/create/#rollback-config", + docDescription: + "Configure automated rollback on update failure. Similar to update config but applies to rollback operations.", + }, + { + id: "mode", + label: "Mode", + description: "Configure service mode", + docUrl: "https://docs.docker.com/reference/cli/docker/service/create/#mode", + docDescription: + "Set service mode to either 'replicated' (default) with a specified number of tasks, or 'global' (one task per node).", + }, + { + id: "labels", + label: "Labels", + description: "Configure service labels", + docUrl: + "https://docs.docker.com/reference/cli/docker/service/create/#label", + docDescription: + "Add metadata to services using labels. Labels are key-value pairs for organizing and filtering services.", }, - { id: "mode", label: "Mode", description: "Configure service mode" }, - { id: "labels", label: "Labels", description: "Configure service labels" }, { id: "stop-grace-period", label: "Stop Grace Period", description: "Configure stop grace period", + docUrl: + "https://docs.docker.com/reference/cli/docker/service/create/#stop-grace-period", + docDescription: + "Time to wait before forcefully killing a container. Given in nanoseconds. Default is 10 seconds.", }, { id: "endpoint-spec", label: "Endpoint Spec", description: "Configure endpoint specification", + docUrl: + "https://docs.docker.com/reference/cli/docker/service/create/#endpoint-mode", + docDescription: + "Configure endpoint mode for service discovery. Choose between 'vip' (virtual IP) or 'dnsrr' (DNS round-robin).", }, ]; @@ -110,22 +161,48 @@ export const AddSwarmSettings = ({ id, type }: Props) => { {/* Left Column - Menu */}
diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/rollback-config-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/rollback-config-form.tsx index c9c6ad128..d53215348 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/rollback-config-form.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/rollback-config-form.tsx @@ -103,7 +103,7 @@ export const RollbackConfigForm = ({ id, type }: RollbackConfigFormProps) => { mysqlId: id || "", mariadbId: id || "", mongoId: id || "", - rollbackConfigSwarm: hasAnyValue ? formData : null, + rollbackConfigSwarm: (hasAnyValue ? formData : null) as any, }); toast.success("Rollback config updated successfully"); diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/update-config-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/update-config-form.tsx index 26c42adff..4119c41f8 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/update-config-form.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/update-config-form.tsx @@ -109,7 +109,7 @@ export const UpdateConfigForm = ({ id, type }: UpdateConfigFormProps) => { mysqlId: id || "", mariadbId: id || "", mongoId: id || "", - updateConfigSwarm: hasAnyValue ? formData : null, + updateConfigSwarm: (hasAnyValue ? formData : null) as any, }); toast.success("Update config updated successfully"); From 983c8d5e9eec268b277eef3ab438470be0cf4061 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 20 Jan 2026 16:31:33 +0100 Subject: [PATCH 18/40] refactor(cluster): streamline swarm settings documentation and UI components - Removed unused documentation URLs from menu items in swarm settings. - Enhanced doc descriptions for better clarity on configuration options. - Refactored tooltip implementation for improved UI consistency. --- .../cluster/modify-swarm-settings.tsx | 98 ++++++------------- 1 file changed, 32 insertions(+), 66 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx index aa96d320d..ee427feca 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx @@ -1,4 +1,4 @@ -import { ExternalLink, Settings } from "lucide-react"; +import { Settings } from "lucide-react"; import { useState } from "react"; import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; @@ -33,8 +33,7 @@ type MenuItem = { id: string; label: string; description: string; - docUrl?: string; - docDescription?: string; + docDescription: string; }; const menuItems: MenuItem[] = [ @@ -42,81 +41,64 @@ const menuItems: MenuItem[] = [ id: "health-check", label: "Health Check", description: "Configure health check settings", - docUrl: - "https://docs.docker.com/reference/cli/docker/service/create/#healthcheck", docDescription: - "Configure HEALTHCHECK to test a container's health. Determines if a container is healthy by running a command inside the container.", + "Configure HEALTHCHECK to test a container's health. Determines if a container is healthy by running a command inside the container. Test, Interval, Timeout, StartPeriod, and Retries control health monitoring.", }, { id: "restart-policy", label: "Restart Policy", description: "Configure restart policy", - docUrl: - "https://docs.docker.com/reference/cli/docker/service/create/#restart-policy", docDescription: - "Configure the restart policy for containers in the service. Controls when and how containers should be restarted.", + "Configure the restart policy for containers in the service. Condition (none, on-failure, any), Delay (nanoseconds between restarts), MaxAttempts, and Window control restart behavior.", }, { id: "placement", label: "Placement", description: "Configure placement constraints", - docUrl: - "https://docs.docker.com/reference/cli/docker/service/create/#placement-pref", docDescription: - "Control which nodes service tasks can be scheduled on. Use constraints, preferences, and platform specifications.", + "Control which nodes service tasks can be scheduled on. Constraints (node.id==xyz), Preferences (spread.node.labels.zone), MaxReplicas, and Platforms specify task placement rules.", }, { id: "update-config", label: "Update Config", description: "Configure update strategy", - docUrl: - "https://docs.docker.com/reference/cli/docker/service/create/#update-config", docDescription: - "Configure how the service should be updated. Controls parallelism, delay, failure action, and order of updates.", + "Configure how the service should be updated. Parallelism (tasks updated simultaneously), Delay, FailureAction (pause, continue, rollback), Monitor, MaxFailureRatio, and Order (stop-first, start-first) control updates.", }, { id: "rollback-config", label: "Rollback Config", description: "Configure rollback strategy", - docUrl: - "https://docs.docker.com/reference/cli/docker/service/create/#rollback-config", docDescription: - "Configure automated rollback on update failure. Similar to update config but applies to rollback operations.", + "Configure automated rollback on update failure. Uses same parameters as UpdateConfig: Parallelism, Delay, FailureAction, Monitor, MaxFailureRatio, and Order.", }, { id: "mode", label: "Mode", description: "Configure service mode", - docUrl: "https://docs.docker.com/reference/cli/docker/service/create/#mode", docDescription: - "Set service mode to either 'replicated' (default) with a specified number of tasks, or 'global' (one task per node).", + "Set service mode to either 'Replicated' with a specified number of tasks (Replicas), or 'Global' (one task per node).", }, { id: "labels", label: "Labels", description: "Configure service labels", - docUrl: - "https://docs.docker.com/reference/cli/docker/service/create/#label", docDescription: - "Add metadata to services using labels. Labels are key-value pairs for organizing and filtering services.", + "Add metadata to services using labels. Labels are key-value pairs (e.g., com.example.foo=bar) for organizing and filtering services.", }, { id: "stop-grace-period", label: "Stop Grace Period", description: "Configure stop grace period", - docUrl: - "https://docs.docker.com/reference/cli/docker/service/create/#stop-grace-period", docDescription: - "Time to wait before forcefully killing a container. Given in nanoseconds. Default is 10 seconds.", + "Time to wait before forcefully killing a container. Specified in nanoseconds (e.g., 10000000000 = 10 seconds). Allows containers to shutdown gracefully.", }, { id: "endpoint-spec", label: "Endpoint Spec", description: "Configure endpoint specification", - docUrl: - "https://docs.docker.com/reference/cli/docker/service/create/#endpoint-mode", docDescription: - "Configure endpoint mode for service discovery. Choose between 'vip' (virtual IP) or 'dnsrr' (DNS round-robin).", + "Configure endpoint mode for service discovery. Mode 'vip' (virtual IP - default) uses a single virtual IP. Mode 'dnsrr' (DNS round-robin) returns DNS entries for all tasks.", }, ]; @@ -163,44 +145,28 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
+ > +
{item.label}
+
+ {item.description} +
+ + + +

{item.docDescription}

+
+ ))} From 9c565656b1adc76a35b431de9c219abae835a166 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 20 Jan 2026 16:33:42 +0100 Subject: [PATCH 19/40] feat(dashboard): hide builder section for Docker source type - Added logic to conditionally hide the builder section when the Docker provider is selected, improving user experience by reducing unnecessary UI elements. --- apps/dokploy/components/dashboard/application/build/show.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/dokploy/components/dashboard/application/build/show.tsx b/apps/dokploy/components/dashboard/application/build/show.tsx index 3dd030c4e..7f92157f2 100644 --- a/apps/dokploy/components/dashboard/application/build/show.tsx +++ b/apps/dokploy/components/dashboard/application/build/show.tsx @@ -207,6 +207,11 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { } }, [data, form]); + // Hide builder section when Docker provider is selected + if (data?.sourceType === "docker") { + return null; + } + const onSubmit = async (data: AddTemplate) => { await mutateAsync({ applicationId, From 36f082f12ab7c0c7d9504a877c00a13bc56203ec Mon Sep 17 00:00:00 2001 From: Mika Andrianarijaona Date: Tue, 20 Jan 2026 17:13:14 +0100 Subject: [PATCH 20/40] fix: replace truncate with break-all --- apps/dokploy/components/dashboard/projects/show.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index c962053b3..51b0f170a 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -419,7 +419,7 @@ export const ShowProjects = () => { ) : null} - +
@@ -427,7 +427,7 @@ export const ShowProjects = () => {
- + {project.description} From a8fc2adab604dac0213533a2708d9c278f658032 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 21 Jan 2026 11:22:52 +0100 Subject: [PATCH 21/40] feat(dashboard): add environment availability alert for projects - Implemented a check for projects with no accessible environments, displaying an alert message to inform users. - Updated project link behavior to prevent navigation when no environments are available, enhancing user experience. --- .../components/dashboard/projects/show.tsx | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index 740c75d8f..8234593e1 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -293,13 +293,24 @@ export const ShowProjects = () => { project?.environments.find((env) => env.isDefault) || project?.environments?.[0]; + const hasNoEnvironments = !accessibleEnvironment; + return (
{ + if (hasNoEnvironments) { + e.preventDefault(); + } + }} > {haveServicesWithDomains ? ( @@ -431,6 +442,16 @@ export const ShowProjects = () => { {project.description} + + {hasNoEnvironments && ( +
+ + + You have access to this project but no + environments are available + +
+ )}
From 8f2a0f80296b29061673d55eba1bbca132bb309a Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 21 Jan 2026 13:29:32 +0100 Subject: [PATCH 22/40] feat(db): enhance database configuration with environment variable support - Introduced a function to read database credentials from a file for improved security. - Added support for environment variables to configure database connection, replacing hardcoded values. - Implemented a warning for users relying on deprecated hardcoded credentials, encouraging migration to Docker Secrets. --- apps/dokploy/server/db/index.ts | 43 ++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/server/db/index.ts b/apps/dokploy/server/db/index.ts index 55d6d3a46..644bda54c 100644 --- a/apps/dokploy/server/db/index.ts +++ b/apps/dokploy/server/db/index.ts @@ -1,3 +1,4 @@ +import fs from "node:fs"; import { drizzle, type PostgresJsDatabase } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "./schema"; @@ -6,9 +7,45 @@ declare global { var db: PostgresJsDatabase | undefined; } -const dbUrl = - process.env.DATABASE_URL || - "postgres://dokploy:amukds4wi9001583845717ad2@dokploy-postgres:5432/dokploy"; +function readSecret(path: string): string { + try { + return fs.readFileSync(path, "utf8").trim(); + } catch { + throw new Error(`Cannot read secret at ${path}`); + } +} + +const { + DATABASE_URL, + POSTGRES_PASSWORD_FILE, + POSTGRES_USER = "dokploy", + POSTGRES_DB = "dokploy", + POSTGRES_HOST = "dokploy-postgres", + POSTGRES_PORT = "5432", +} = process.env; + +let dbUrl: string; + +if (DATABASE_URL) { + // Compatibilidad legacy / overrides + dbUrl = DATABASE_URL; +} else if (POSTGRES_PASSWORD_FILE) { + const password = readSecret(POSTGRES_PASSWORD_FILE); + dbUrl = `postgres://${POSTGRES_USER}:${encodeURIComponent( + password, + )}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}`; +} else { + console.warn(` + ⚠️ [DEPRECATED DATABASE CONFIG] + You are using the legacy hardcoded database credentials. + This mode WILL BE REMOVED in a future release. + + Please migrate to Docker Secrets using POSTGRES_PASSWORD_FILE. + Please execute this guide: https://dokploy.com/SECURITY_MIGRATION.md + `); + dbUrl = + "postgres://dokploy:amukds4wi9001583845717ad2@dokploy-postgres:5432/dokploy"; +} export let db: PostgresJsDatabase; if (process.env.NODE_ENV === "production") { From c8ec86c63928d1048bb1876a7af288eca1e279a2 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 21 Jan 2026 16:56:30 +0100 Subject: [PATCH 23/40] chore(env): remove hardcoded DATABASE_URL from production example file --- apps/dokploy/.env.production.example | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/dokploy/.env.production.example b/apps/dokploy/.env.production.example index 41e934c3a..560faf9e6 100644 --- a/apps/dokploy/.env.production.example +++ b/apps/dokploy/.env.production.example @@ -1,3 +1,2 @@ -DATABASE_URL="postgres://dokploy:amukds4wi9001583845717ad2@dokploy-postgres:5432/dokploy" PORT=3000 NODE_ENV=production \ No newline at end of file From cbd70fe5d0ce68c492606a0d312616978343f88f Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 21 Jan 2026 17:19:28 +0100 Subject: [PATCH 24/40] refactor(db): replace hardcoded DATABASE_URL with dbUrl import for improved configuration --- apps/dokploy/migration.ts | 3 ++- apps/dokploy/server/db/index.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/migration.ts b/apps/dokploy/migration.ts index d52066c88..dd312b805 100644 --- a/apps/dokploy/migration.ts +++ b/apps/dokploy/migration.ts @@ -1,8 +1,9 @@ import { drizzle } from "drizzle-orm/postgres-js"; import { migrate } from "drizzle-orm/postgres-js/migrator"; import postgres from "postgres"; +import { dbUrl } from "./server/db"; -const connectionString = process.env.DATABASE_URL!; +const connectionString = dbUrl; const sql = postgres(connectionString, { max: 1 }); const db = drizzle(sql); diff --git a/apps/dokploy/server/db/index.ts b/apps/dokploy/server/db/index.ts index 644bda54c..618ff772a 100644 --- a/apps/dokploy/server/db/index.ts +++ b/apps/dokploy/server/db/index.ts @@ -24,7 +24,7 @@ const { POSTGRES_PORT = "5432", } = process.env; -let dbUrl: string; +export let dbUrl: string; if (DATABASE_URL) { // Compatibilidad legacy / overrides From 9a9e3dc295adbfffaae0281733500bb659c1ee9c Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 21 Jan 2026 17:33:06 +0100 Subject: [PATCH 25/40] refactor(db): centralize database URL configuration by importing dbUrl from constants --- apps/dokploy/migration.ts | 6 ++-- apps/dokploy/server/db/drizzle.config.ts | 3 +- apps/dokploy/server/db/index.ts | 42 +----------------------- apps/dokploy/server/db/migration.ts | 21 ------------ apps/dokploy/server/db/reset.ts | 5 ++- packages/server/src/db/constants.ts | 39 ++++++++++++++++++++++ packages/server/src/db/index.ts | 7 ++-- packages/server/src/index.ts | 1 + 8 files changed, 52 insertions(+), 72 deletions(-) delete mode 100644 apps/dokploy/server/db/migration.ts create mode 100644 packages/server/src/db/constants.ts diff --git a/apps/dokploy/migration.ts b/apps/dokploy/migration.ts index dd312b805..984197b2a 100644 --- a/apps/dokploy/migration.ts +++ b/apps/dokploy/migration.ts @@ -1,11 +1,9 @@ +import { dbUrl } from "@dokploy/server/db"; import { drizzle } from "drizzle-orm/postgres-js"; import { migrate } from "drizzle-orm/postgres-js/migrator"; import postgres from "postgres"; -import { dbUrl } from "./server/db"; -const connectionString = dbUrl; - -const sql = postgres(connectionString, { max: 1 }); +const sql = postgres(dbUrl, { max: 1 }); const db = drizzle(sql); await migrate(db, { migrationsFolder: "drizzle" }) diff --git a/apps/dokploy/server/db/drizzle.config.ts b/apps/dokploy/server/db/drizzle.config.ts index 60a3bb937..8f6a4a60a 100644 --- a/apps/dokploy/server/db/drizzle.config.ts +++ b/apps/dokploy/server/db/drizzle.config.ts @@ -1,10 +1,11 @@ +import { dbUrl } from "@dokploy/server/db"; import { defineConfig } from "drizzle-kit"; export default defineConfig({ schema: "./server/db/schema/index.ts", dialect: "postgresql", dbCredentials: { - url: process.env.DATABASE_URL!, + url: dbUrl, }, out: "drizzle", migrations: { diff --git a/apps/dokploy/server/db/index.ts b/apps/dokploy/server/db/index.ts index 618ff772a..2112c4f67 100644 --- a/apps/dokploy/server/db/index.ts +++ b/apps/dokploy/server/db/index.ts @@ -1,4 +1,4 @@ -import fs from "node:fs"; +import { dbUrl } from "@dokploy/server/db/constants"; import { drizzle, type PostgresJsDatabase } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "./schema"; @@ -7,46 +7,6 @@ declare global { var db: PostgresJsDatabase | undefined; } -function readSecret(path: string): string { - try { - return fs.readFileSync(path, "utf8").trim(); - } catch { - throw new Error(`Cannot read secret at ${path}`); - } -} - -const { - DATABASE_URL, - POSTGRES_PASSWORD_FILE, - POSTGRES_USER = "dokploy", - POSTGRES_DB = "dokploy", - POSTGRES_HOST = "dokploy-postgres", - POSTGRES_PORT = "5432", -} = process.env; - -export let dbUrl: string; - -if (DATABASE_URL) { - // Compatibilidad legacy / overrides - dbUrl = DATABASE_URL; -} else if (POSTGRES_PASSWORD_FILE) { - const password = readSecret(POSTGRES_PASSWORD_FILE); - dbUrl = `postgres://${POSTGRES_USER}:${encodeURIComponent( - password, - )}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}`; -} else { - console.warn(` - ⚠️ [DEPRECATED DATABASE CONFIG] - You are using the legacy hardcoded database credentials. - This mode WILL BE REMOVED in a future release. - - Please migrate to Docker Secrets using POSTGRES_PASSWORD_FILE. - Please execute this guide: https://dokploy.com/SECURITY_MIGRATION.md - `); - dbUrl = - "postgres://dokploy:amukds4wi9001583845717ad2@dokploy-postgres:5432/dokploy"; -} - export let db: PostgresJsDatabase; if (process.env.NODE_ENV === "production") { db = drizzle(postgres(dbUrl!), { diff --git a/apps/dokploy/server/db/migration.ts b/apps/dokploy/server/db/migration.ts deleted file mode 100644 index fa2e1a80f..000000000 --- a/apps/dokploy/server/db/migration.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { drizzle } from "drizzle-orm/postgres-js"; -import { migrate } from "drizzle-orm/postgres-js/migrator"; -import postgres from "postgres"; - -const connectionString = process.env.DATABASE_URL!; - -const sql = postgres(connectionString, { max: 1 }); -const db = drizzle(sql); - -export const migration = async () => - await migrate(db, { migrationsFolder: "drizzle" }) - .then(() => { - console.log("Migration complete"); - sql.end(); - }) - .catch((error) => { - console.log("Migration failed", error); - }) - .finally(() => { - sql.end(); - }); diff --git a/apps/dokploy/server/db/reset.ts b/apps/dokploy/server/db/reset.ts index c22291478..4c6e3736e 100644 --- a/apps/dokploy/server/db/reset.ts +++ b/apps/dokploy/server/db/reset.ts @@ -1,11 +1,10 @@ +import { dbUrl } from "@dokploy/server/db"; import { sql } from "drizzle-orm"; // Credits to Louistiti from Drizzle Discord: https://discord.com/channels/1043890932593987624/1130802621750448160/1143083373535973406 import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; -const connectionString = process.env.DATABASE_URL!; - -const pg = postgres(connectionString, { max: 1 }); +const pg = postgres(dbUrl, { max: 1 }); const db = drizzle(pg); const clearDb = async (): Promise => { diff --git a/packages/server/src/db/constants.ts b/packages/server/src/db/constants.ts new file mode 100644 index 000000000..862288100 --- /dev/null +++ b/packages/server/src/db/constants.ts @@ -0,0 +1,39 @@ +import fs from "node:fs"; + +export const { + DATABASE_URL, + POSTGRES_PASSWORD_FILE, + POSTGRES_USER = "dokploy", + POSTGRES_DB = "dokploy", + POSTGRES_HOST = "dokploy-postgres", + POSTGRES_PORT = "5432", +} = process.env; + +function readSecret(path: string): string { + try { + return fs.readFileSync(path, "utf8").trim(); + } catch { + throw new Error(`Cannot read secret at ${path}`); + } +} +export let dbUrl: string; +if (DATABASE_URL) { + // Compatibilidad legacy / overrides + dbUrl = DATABASE_URL; +} else if (POSTGRES_PASSWORD_FILE) { + const password = readSecret(POSTGRES_PASSWORD_FILE); + dbUrl = `postgres://${POSTGRES_USER}:${encodeURIComponent( + password, + )}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}`; +} else { + console.warn(` + ⚠️ [DEPRECATED DATABASE CONFIG] + You are using the legacy hardcoded database credentials. + This mode WILL BE REMOVED in a future release. + + Please migrate to Docker Secrets using POSTGRES_PASSWORD_FILE. + Please execute this guide: https://dokploy.com/SECURITY_MIGRATION.md + `); + dbUrl = + "postgres://dokploy:amukds4wi9001583845717ad2@dokploy-postgres:5432/dokploy"; +} diff --git a/packages/server/src/db/index.ts b/packages/server/src/db/index.ts index 3ac6e3940..e17002de9 100644 --- a/packages/server/src/db/index.ts +++ b/packages/server/src/db/index.ts @@ -1,5 +1,6 @@ import { drizzle, type PostgresJsDatabase } from "drizzle-orm/postgres-js"; import postgres from "postgres"; +import { dbUrl } from "./constants"; import * as schema from "./schema"; declare global { @@ -8,14 +9,16 @@ declare global { export let db: PostgresJsDatabase; if (process.env.NODE_ENV === "production") { - db = drizzle(postgres(process.env.DATABASE_URL!), { + db = drizzle(postgres(dbUrl), { schema, }); } else { if (!global.db) - global.db = drizzle(postgres(process.env.DATABASE_URL!), { + global.db = drizzle(postgres(dbUrl), { schema, }); db = global.db; } + +export { dbUrl }; diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index f28711dbf..c05ac1ab7 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,5 +1,6 @@ export * from "./auth/random-password"; export * from "./constants/index"; +export * from "./db/constants"; export * from "./db/validations/domain"; export * from "./db/validations/index"; export * from "./lib/auth"; From dbd354d928a690295e007db6855c7e899dc6925f Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 21 Jan 2026 17:55:59 +0100 Subject: [PATCH 26/40] refactor(db): centralize database URL configuration by importing dbUrl from constants --- apps/dokploy/server/db/migration.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 apps/dokploy/server/db/migration.ts diff --git a/apps/dokploy/server/db/migration.ts b/apps/dokploy/server/db/migration.ts new file mode 100644 index 000000000..8a24afdc5 --- /dev/null +++ b/apps/dokploy/server/db/migration.ts @@ -0,0 +1,20 @@ +import { dbUrl } from "@dokploy/server/db"; +import { drizzle } from "drizzle-orm/postgres-js"; +import { migrate } from "drizzle-orm/postgres-js/migrator"; +import postgres from "postgres"; + +const sql = postgres(dbUrl, { max: 1 }); +const db = drizzle(sql); + +export const migration = async () => + await migrate(db, { migrationsFolder: "drizzle" }) + .then(() => { + console.log("Migration complete"); + sql.end(); + }) + .catch((error) => { + console.log("Migration failed", error); + }) + .finally(() => { + sql.end(); + }); From 86548a1f248055b1ed91968da5205afb4be8fc15 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 21 Jan 2026 18:07:51 +0100 Subject: [PATCH 27/40] chore(package): update dokploy version to v0.26.6 --- 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 7fe165a42..758f4697a 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.26.5", + "version": "v0.26.6", "private": true, "license": "Apache-2.0", "type": "module", From 733f4c4a23adb275df4b322ec9fb9125baa099a6 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 21 Jan 2026 18:23:32 +0100 Subject: [PATCH 28/40] fix(db): update security migration command for database configuration --- packages/server/src/db/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/db/constants.ts b/packages/server/src/db/constants.ts index 862288100..1d4ec2f1f 100644 --- a/packages/server/src/db/constants.ts +++ b/packages/server/src/db/constants.ts @@ -32,7 +32,7 @@ if (DATABASE_URL) { This mode WILL BE REMOVED in a future release. Please migrate to Docker Secrets using POSTGRES_PASSWORD_FILE. - Please execute this guide: https://dokploy.com/SECURITY_MIGRATION.md + Please execute this command in your server: curl -sSL https://dokploy.com/security/0.26.6.sh | bash `); dbUrl = "postgres://dokploy:amukds4wi9001583845717ad2@dokploy-postgres:5432/dokploy"; From 37e817ff261ff788f80923bbbe6d25f2879094d4 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 21 Jan 2026 18:52:57 +0100 Subject: [PATCH 29/40] feat(config): add security headers to enhance application security --- apps/dokploy/next.config.mjs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/apps/dokploy/next.config.mjs b/apps/dokploy/next.config.mjs index a1b19d722..48231114a 100644 --- a/apps/dokploy/next.config.mjs +++ b/apps/dokploy/next.config.mjs @@ -19,6 +19,32 @@ const nextConfig = { locales: ["en"], defaultLocale: "en", }, + async headers() { + return [ + { + // Apply security headers to all routes + source: "/:path*", + headers: [ + { + key: "X-Frame-Options", + value: "DENY", + }, + { + key: "Content-Security-Policy", + value: "frame-ancestors 'none'", + }, + { + key: "X-Content-Type-Options", + value: "nosniff", + }, + { + key: "Referrer-Policy", + value: "strict-origin-when-cross-origin", + }, + ], + }, + ]; + }, }; export default nextConfig; From dd10d0b1a41f079d791280169197f939f889afdd Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 21 Jan 2026 19:43:33 +0100 Subject: [PATCH 30/40] feat(license): introduce proprietary license and update core license terms --- LICENSE.MD | 19 ++++++++----------- LICENSE_PROPRIETARY.md | 11 +++++++++++ apps/dokploy/proprietary/README.md | 18 ++++++++++++++++++ 3 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 LICENSE_PROPRIETARY.md create mode 100644 apps/dokploy/proprietary/README.md diff --git a/LICENSE.MD b/LICENSE.MD index 6cbef2c6d..bcef8b36e 100644 --- a/LICENSE.MD +++ b/LICENSE.MD @@ -1,8 +1,13 @@ -# License +Copyright 2026-present Dokploy Technology, Inc. -## Core License (Apache License 2.0) +Portions of this software are licensed as follows: -Copyright 2025 Mauricio Siu. +* All content that resides under a "/proprietary" directory of this repository, if that directory exists, is licensed under the license defined in "LICENSE_PROPRIETARY". +* Content outside of the above mentioned directories or restrictions above is available under the "Apache License 2.0" license as defined below. + +## Apache License 2.0 + +Copyright 2026-present Dokploy Technology, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,12 +20,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -## Additional Terms for Specific Features -The following additional terms apply to the multi-node support, Docker Compose file, Preview Deployments and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License: - -- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Schedules, Preview Deployments and Multi Server, will always be free to use in the self-hosted version. -- **Restriction on Resale**: The multi-node support, Docker Compose file support, Schedules, Preview Deployments and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent. -- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support, Schedules, Preview Deployments and Multi Server features must be distributed freely and cannot be sold or offered as a service. - -For further inquiries or permissions, please contact us directly. diff --git a/LICENSE_PROPRIETARY.md b/LICENSE_PROPRIETARY.md new file mode 100644 index 000000000..0f4957575 --- /dev/null +++ b/LICENSE_PROPRIETARY.md @@ -0,0 +1,11 @@ +The Dokploy Source Available license (DSAL) version 1.0 + +Copyright (c) 2026-present Dokploy Technology, Inc. + +With regard to the Dokploy Software:This software and associated documentation files (the "Software") may only beused in production, if you (and any entity that you represent) have agreed to, and are in compliance with, a valid commercial agreement from Dokploy.Subject to the foregoing sentence, you are free to modify this Software and publish patches to the Software. You agree that Dokploy and/or its licensors (as applicable) retain all right, title and interest in and to all such modifications and/or patches, and all such modifications and/or patches may only be used, copied, modified, displayed, distributed, or otherwise exploited with a valid Dokploy Source Available License.  Notwithstanding the foregoing, you may copy and modify the Software for development and testing purposes, without requiring a subscription.  You agree that Dokploy and/or its licensors (as applicable) retain all right, title and interest in and to all such modifications.  You are not granted any other rights beyond what is expressly stated herein.  Subject to theforegoing, it is forbidden to copy, merge, publish, distribute, sublicense,and/or sell the Software. + +This Dokploy Source Available license applies only to the part of this Software that is in a /proprietary folder. The full text of this License shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS ORIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THEAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHERLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THESOFTWARE. + +For all third party components incorporated into the Dokploy Software, thosecomponents are licensed under the original license provided by the owner of the applicable component. \ No newline at end of file diff --git a/apps/dokploy/proprietary/README.md b/apps/dokploy/proprietary/README.md new file mode 100644 index 000000000..b1af288e6 --- /dev/null +++ b/apps/dokploy/proprietary/README.md @@ -0,0 +1,18 @@ +# Proprietary Features + +This directory contains all proprietary functionality of Dokploy. + +## Purpose + +This folder will house all **paid features** and premium functionality that are not part of the open source code. + +## License + +The code in this directory is under Dokploy's proprietary license. See [LICENSE_PROPRIETARY.md](../../../LICENSE_PROPRIETARY.md) for more details. + +## Contact + +If you want to learn more about our paid features or have any questions, please contact us at: + +- Email: [sales@dokploy.com](mailto:sales@dokploy.com) +- Contact Form: [https://dokploy.com/contact](https://dokploy.com/contact) From bcbf43360722bf816b1a98534d2226a4e4dd9173 Mon Sep 17 00:00:00 2001 From: Bima42 Date: Thu, 22 Jan 2026 08:56:07 +0100 Subject: [PATCH 31/40] fix: zod object for assign domain --- packages/server/src/db/schema/web-server-settings.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/server/src/db/schema/web-server-settings.ts b/packages/server/src/db/schema/web-server-settings.ts index 92219091d..fe5cc5ad1 100644 --- a/packages/server/src/db/schema/web-server-settings.ts +++ b/packages/server/src/db/schema/web-server-settings.ts @@ -131,7 +131,10 @@ export const apiAssignDomain = z .object({ host: z.string(), certificateType: z.enum(["letsencrypt", "none", "custom"]), - letsEncryptEmail: z.string().email().optional().nullable(), + letsEncryptEmail: z + .union([z.string().email(), z.literal("")]) + .optional() + .nullable(), https: z.boolean().optional(), }) .required() From 84fa805acc796219cca9d5ee53927602787b9856 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 25 Jan 2026 17:53:16 +0200 Subject: [PATCH 32/40] refactor(side): remove Sponsor menu item and associated HeartIcon component --- apps/dokploy/components/layouts/side.tsx | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index 45b6a7e3a..d256a5119 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -18,7 +18,6 @@ import { Forward, GalleryVerticalEnd, GitBranch, - HeartIcon, KeyRound, Loader2, type LucideIcon, @@ -410,18 +409,6 @@ const MENU: Menu = { url: "https://discord.gg/2tBnJ3jDJc", icon: CircleHelp, }, - { - name: "Sponsor", - url: "https://opencollective.com/dokploy", - icon: ({ className }) => ( - - ), - }, ], } as const; From 7362cc49d2b7b57630a70756063cc5ecc2dbf3fc Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Mon, 26 Jan 2026 16:37:15 +0200 Subject: [PATCH 33/40] fix: prevent to pass invalid docker container names --- .../server/wss/docker-container-logs.ts | 7 +- .../server/wss/docker-container-terminal.ts | 129 +++++++++++------- apps/dokploy/server/wss/docker-stats.ts | 7 + apps/dokploy/server/wss/listen-deployment.ts | 7 +- apps/dokploy/server/wss/utils.ts | 34 ++++- 5 files changed, 132 insertions(+), 52 deletions(-) diff --git a/apps/dokploy/server/wss/docker-container-logs.ts b/apps/dokploy/server/wss/docker-container-logs.ts index eaefa21f1..77fba8bb5 100644 --- a/apps/dokploy/server/wss/docker-container-logs.ts +++ b/apps/dokploy/server/wss/docker-container-logs.ts @@ -1,5 +1,5 @@ import type http from "node:http"; -import { findServerById, validateRequest } from "@dokploy/server"; +import { findServerById, IS_CLOUD, validateRequest } from "@dokploy/server"; import { spawn } from "node-pty"; import { Client } from "ssh2"; import { WebSocketServer } from "ws"; @@ -111,6 +111,11 @@ export const setupDockerContainerLogsWebSocketServer = ( client.end(); }); } else { + if (IS_CLOUD) { + ws.send("This feature is not available in the cloud version."); + ws.close(); + return; + } const shell = getShell(); const baseCommand = `docker ${runType === "swarm" ? "service" : "container"} logs --timestamps ${ runType === "swarm" ? "--raw" : "" diff --git a/apps/dokploy/server/wss/docker-container-terminal.ts b/apps/dokploy/server/wss/docker-container-terminal.ts index 155d7f0cc..2bdaaf73d 100644 --- a/apps/dokploy/server/wss/docker-container-terminal.ts +++ b/apps/dokploy/server/wss/docker-container-terminal.ts @@ -1,9 +1,9 @@ import type http from "node:http"; -import { findServerById, validateRequest } from "@dokploy/server"; +import { findServerById, IS_CLOUD, validateRequest } from "@dokploy/server"; import { spawn } from "node-pty"; import { Client } from "ssh2"; import { WebSocketServer } from "ws"; -import { getShell } from "./utils"; +import { getShell, isValidContainerId, isValidShell } from "./utils"; export const setupDockerContainerTerminalWebSocketServer = ( server: http.Server, @@ -39,6 +39,26 @@ export const setupDockerContainerTerminalWebSocketServer = ( return; } + if (!containerId) { + ws.close(4000, "containerId not provided"); + return; + } + + // Security: Validate containerId to prevent command injection + if (!isValidContainerId(containerId)) { + ws.close(4000, "Invalid container ID format"); + return; + } + + // Security: Validate shell to prevent command injection + if (activeWay && !isValidShell(activeWay)) { + ws.close(4000, "Invalid shell specified"); + return; + } + + // Default to 'sh' if no shell specified + const shell = activeWay || "sh"; + if (!user || !session) { ws.close(); return; @@ -54,55 +74,61 @@ export const setupDockerContainerTerminalWebSocketServer = ( let _stderr = ""; conn .once("ready", () => { - conn.exec( - `docker exec -it -w / ${containerId} ${activeWay}`, - { pty: true }, - (err, stream) => { - if (err) { - console.error("SSH exec error:", err); - ws.close(); + // Use array-style arguments to prevent shell injection + const dockerCommand = [ + "docker", + "exec", + "-it", + "-w", + "/", + containerId, + shell, + ].join(" "); + conn.exec(dockerCommand, { pty: true }, (err, stream) => { + if (err) { + console.error("SSH exec error:", err); + ws.close(); + conn.end(); + return; + } + + stream + .on("close", (code: number, _signal: string) => { + ws.send(`\nContainer closed with code: ${code}\n`); conn.end(); - return; - } + }) + .on("data", (data: string) => { + _stdout += data.toString(); + ws.send(data.toString()); + }) + .stderr.on("data", (data) => { + _stderr += data.toString(); + ws.send(data.toString()); + console.error("Error: ", data.toString()); + }); - stream - .on("close", (code: number, _signal: string) => { - ws.send(`\nContainer closed with code: ${code}\n`); - conn.end(); - }) - .on("data", (data: string) => { - _stdout += data.toString(); - ws.send(data.toString()); - }) - .stderr.on("data", (data) => { - _stderr += data.toString(); - ws.send(data.toString()); - console.error("Error: ", data.toString()); - }); - - ws.on("message", (message) => { - try { - let command: string | Buffer[] | Buffer | ArrayBuffer; - if (Buffer.isBuffer(message)) { - command = message.toString("utf8"); - } else { - command = message; - } - stream.write(command.toString()); - } catch (error) { - // @ts-ignore - const errorMessage = error?.message as unknown as string; - ws.send(errorMessage); + ws.on("message", (message) => { + try { + let command: string | Buffer[] | Buffer | ArrayBuffer; + if (Buffer.isBuffer(message)) { + command = message.toString("utf8"); + } else { + command = message; } - }); + stream.write(command.toString()); + } catch (error) { + // @ts-ignore + const errorMessage = error?.message as unknown as string; + ws.send(errorMessage); + } + }); - ws.on("close", () => { - stream.end(); - // Ensure SSH connection is closed when WebSocket closes - conn.end(); - }); - }, - ); + ws.on("close", () => { + stream.end(); + // Ensure SSH connection is closed when WebSocket closes + conn.end(); + }); + }); }) .on("error", (err) => { console.error("SSH connection error:", err); @@ -119,10 +145,15 @@ export const setupDockerContainerTerminalWebSocketServer = ( privateKey: server.sshKey?.privateKey, }); } else { + if (IS_CLOUD) { + ws.send("This feature is not available in the cloud version."); + ws.close(); + return; + } const shell = getShell(); const ptyProcess = spawn( - shell, - ["-c", `docker exec -it -w / ${containerId} ${activeWay}`], + "docker", + ["exec", "-it", "-w", "/", containerId, shell], {}, ); diff --git a/apps/dokploy/server/wss/docker-stats.ts b/apps/dokploy/server/wss/docker-stats.ts index bd740e976..b5f2439bf 100644 --- a/apps/dokploy/server/wss/docker-stats.ts +++ b/apps/dokploy/server/wss/docker-stats.ts @@ -4,6 +4,7 @@ import { execAsync, getHostSystemStats, getLastAdvancedStatsFile, + IS_CLOUD, recordAdvancedStats, validateRequest, } from "@dokploy/server"; @@ -32,6 +33,12 @@ export const setupDockerStatsMonitoringSocketServer = ( wssTerm.on("connection", async (ws, req) => { const url = new URL(req.url || "", `http://${req.headers.host}`); + + if (IS_CLOUD) { + ws.send("This feature is not available in the cloud version."); + ws.close(); + return; + } const appName = url.searchParams.get("appName"); const appType = (url.searchParams.get("appType") || "application") as | "application" diff --git a/apps/dokploy/server/wss/listen-deployment.ts b/apps/dokploy/server/wss/listen-deployment.ts index ca49cea29..75ddf7d1d 100644 --- a/apps/dokploy/server/wss/listen-deployment.ts +++ b/apps/dokploy/server/wss/listen-deployment.ts @@ -1,6 +1,6 @@ import { spawn } from "node:child_process"; import type http from "node:http"; -import { findServerById, validateRequest } from "@dokploy/server"; +import { findServerById, IS_CLOUD, validateRequest } from "@dokploy/server"; import { Client } from "ssh2"; import { WebSocketServer } from "ws"; @@ -108,6 +108,11 @@ export const setupDeploymentLogsWebSocketServer = ( } }); } else { + if (IS_CLOUD) { + ws.send("This feature is not available in the cloud version."); + ws.close(); + return; + } tailProcess = spawn("tail", ["-n", "+1", "-f", logPath]); const stdout = tailProcess.stdout; diff --git a/apps/dokploy/server/wss/utils.ts b/apps/dokploy/server/wss/utils.ts index 1a65fc520..7ec0f9ce8 100644 --- a/apps/dokploy/server/wss/utils.ts +++ b/apps/dokploy/server/wss/utils.ts @@ -1,9 +1,41 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { execAsync, paths } from "@dokploy/server"; +import { execAsync, IS_CLOUD, paths } from "@dokploy/server"; + +/** + * Validates that the container ID matches Docker's expected format. + * Docker container IDs are 64-character hex strings (or 12-char short form). + * Also allows container names: alphanumeric, underscores, hyphens, and dots. + */ +export const isValidContainerId = (id: string): boolean => { + // Match full ID (64 hex chars), short ID (12 hex chars), or container name + const hexPattern = /^[a-f0-9]{12,64}$/i; + const namePattern = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/; + return hexPattern.test(id) || (namePattern.test(id) && id.length <= 128); +}; + +/** + * Validates that the shell is one of the allowed shells. + */ +export const isValidShell = (shell: string): boolean => { + const allowedShells = [ + "sh", + "bash", + "zsh", + "ash", + "/bin/sh", + "/bin/bash", + "/bin/zsh", + "/bin/ash", + ]; + return allowedShells.includes(shell); +}; export const getShell = () => { + if (IS_CLOUD) { + return "CLOUD_VERSION"; + } switch (os.platform()) { case "win32": return "powershell.exe"; From 74aecf6828b66762577169f4679918f4c8c89d5b Mon Sep 17 00:00:00 2001 From: p8008d Date: Tue, 27 Jan 2026 15:07:56 +0200 Subject: [PATCH 34/40] fix: profile firstName field not updating The profile form was sending `name` field but the database column is `firstName`. This caused the firstName to be silently ignored during updates. Changed form field and API schema to use `firstName` to match the database column. --- .../dashboard/settings/profile/profile-form.tsx | 12 ++++++------ packages/server/src/db/schema/user.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx index ad66aa0fa..461a4c17c 100644 --- a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx @@ -41,7 +41,7 @@ const profileSchema = z.object({ password: z.string().nullable(), currentPassword: z.string().nullable(), image: z.string().optional(), - name: z.string().optional(), + firstName: z.string().optional(), lastName: z.string().optional(), allowImpersonation: z.boolean().optional().default(false), }); @@ -91,7 +91,7 @@ export const ProfileForm = () => { image: data?.user?.image || "", currentPassword: "", allowImpersonation: data?.user?.allowImpersonation || false, - name: data?.user?.firstName || "", + firstName: data?.user?.firstName || "", lastName: data?.user?.lastName || "", }, resolver: zodResolver(profileSchema), @@ -106,7 +106,7 @@ export const ProfileForm = () => { image: data?.user?.image || "", currentPassword: form.getValues("currentPassword") || "", allowImpersonation: data?.user?.allowImpersonation, - name: data?.user?.firstName || "", + firstName: data?.user?.firstName || "", lastName: data?.user?.lastName || "", }, { @@ -131,7 +131,7 @@ export const ProfileForm = () => { image: values.image, currentPassword: values.currentPassword || undefined, allowImpersonation: values.allowImpersonation, - name: values.name || undefined, + firstName: values.firstName || undefined, lastName: values.lastName || undefined, }); await refetch(); @@ -141,7 +141,7 @@ export const ProfileForm = () => { password: "", image: values.image, currentPassword: "", - name: values.name || "", + firstName: values.firstName || "", lastName: values.lastName || "", }); } catch (error) { @@ -184,7 +184,7 @@ export const ProfileForm = () => {
( First Name diff --git a/packages/server/src/db/schema/user.ts b/packages/server/src/db/schema/user.ts index 51be7a7ea..088f68efa 100644 --- a/packages/server/src/db/schema/user.ts +++ b/packages/server/src/db/schema/user.ts @@ -214,6 +214,6 @@ export const apiUpdateUser = createSchema.partial().extend({ .optional(), password: z.string().optional(), currentPassword: z.string().optional(), - name: z.string().optional(), + firstName: z.string().optional(), lastName: z.string().optional(), }); From 74e0bd5fe3ef7199f44fcd19c6f5a2f09b806d6f Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 27 Jan 2026 08:37:06 -0600 Subject: [PATCH 35/40] fix(wss): update Docker command execution in terminal setup --- apps/dokploy/server/wss/docker-container-terminal.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/server/wss/docker-container-terminal.ts b/apps/dokploy/server/wss/docker-container-terminal.ts index 2bdaaf73d..efe2d450e 100644 --- a/apps/dokploy/server/wss/docker-container-terminal.ts +++ b/apps/dokploy/server/wss/docker-container-terminal.ts @@ -152,8 +152,8 @@ export const setupDockerContainerTerminalWebSocketServer = ( } const shell = getShell(); const ptyProcess = spawn( - "docker", - ["exec", "-it", "-w", "/", containerId, shell], + shell, + ["-c", `docker exec -it -w / ${containerId} ${activeWay}`], {}, ); From 880a377e54805e3a372701140d19247193991b4b Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 27 Jan 2026 08:38:14 -0600 Subject: [PATCH 36/40] fix(wss): handle cloud version restriction in terminal setup --- apps/dokploy/server/wss/terminal.ts | 7 ++++++- apps/dokploy/server/wss/utils.ts | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/server/wss/terminal.ts b/apps/dokploy/server/wss/terminal.ts index fa37d492b..00b0e2c2c 100644 --- a/apps/dokploy/server/wss/terminal.ts +++ b/apps/dokploy/server/wss/terminal.ts @@ -97,7 +97,12 @@ export const setupTerminalWebSocketServer = ( const isLocalServer = serverId === "local"; - if (isLocalServer && !IS_CLOUD) { + if (isLocalServer) { + if (IS_CLOUD) { + ws.send("This feature is not available in the cloud version."); + ws.close(); + return; + } const port = Number(url.searchParams.get("port")); const username = url.searchParams.get("username"); diff --git a/apps/dokploy/server/wss/utils.ts b/apps/dokploy/server/wss/utils.ts index 7ec0f9ce8..be2197501 100644 --- a/apps/dokploy/server/wss/utils.ts +++ b/apps/dokploy/server/wss/utils.ts @@ -34,7 +34,7 @@ export const isValidShell = (shell: string): boolean => { export const getShell = () => { if (IS_CLOUD) { - return "CLOUD_VERSION"; + return "NO_AVAILABLE"; } switch (os.platform()) { case "win32": From d1553e1bdaa477a1c8649fab80225057e6be8c85 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 27 Jan 2026 08:40:57 -0600 Subject: [PATCH 37/40] fix(wss): add cloud version restriction message in command execution --- packages/server/src/utils/schedules/utils.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/server/src/utils/schedules/utils.ts b/packages/server/src/utils/schedules/utils.ts index 1a43d2af6..64657c9a6 100644 --- a/packages/server/src/utils/schedules/utils.ts +++ b/packages/server/src/utils/schedules/utils.ts @@ -1,6 +1,6 @@ import { createWriteStream } from "node:fs"; import path from "node:path"; -import { paths } from "@dokploy/server/constants"; +import { IS_CLOUD, paths } from "@dokploy/server/constants"; import type { Schedule } from "@dokploy/server/db/schema/schedule"; import { createDeploymentSchedule, @@ -93,6 +93,13 @@ export const runCommand = async (scheduleId: string) => { const writeStream = createWriteStream(deployment.logPath, { flags: "a" }); try { + if (IS_CLOUD) { + writeStream.write( + "This feature is not available in the cloud version.", + ); + writeStream.end(); + return; + } writeStream.write( `docker exec ${containerId} ${shellType} -c ${command}\n`, ); From 15e90e9ca9c061f7008011383b643112ebdf2c18 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 27 Jan 2026 08:59:58 -0600 Subject: [PATCH 38/40] refactor(wss): simplify container ID validation and update Docker command structure --- apps/dokploy/server/wss/docker-container-terminal.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/apps/dokploy/server/wss/docker-container-terminal.ts b/apps/dokploy/server/wss/docker-container-terminal.ts index efe2d450e..a2c242d95 100644 --- a/apps/dokploy/server/wss/docker-container-terminal.ts +++ b/apps/dokploy/server/wss/docker-container-terminal.ts @@ -3,7 +3,7 @@ import { findServerById, IS_CLOUD, validateRequest } from "@dokploy/server"; import { spawn } from "node-pty"; import { Client } from "ssh2"; import { WebSocketServer } from "ws"; -import { getShell, isValidContainerId, isValidShell } from "./utils"; +import { isValidContainerId, isValidShell } from "./utils"; export const setupDockerContainerTerminalWebSocketServer = ( server: http.Server, @@ -34,11 +34,6 @@ export const setupDockerContainerTerminalWebSocketServer = ( const serverId = url.searchParams.get("serverId"); const { user, session } = await validateRequest(req); - if (!containerId) { - ws.close(4000, "containerId no provided"); - return; - } - if (!containerId) { ws.close(4000, "containerId not provided"); return; @@ -150,10 +145,9 @@ export const setupDockerContainerTerminalWebSocketServer = ( ws.close(); return; } - const shell = getShell(); const ptyProcess = spawn( - shell, - ["-c", `docker exec -it -w / ${containerId} ${activeWay}`], + "docker", + ["exec", "-it", "-w", "/", containerId, shell], {}, ); From 24c1c2a37702fef28d6537c15eeab9339228b8b6 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 27 Jan 2026 09:20:29 -0600 Subject: [PATCH 39/40] fix(wss): add container ID validation to enhance security in WebSocket server --- apps/dokploy/server/wss/docker-container-logs.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/server/wss/docker-container-logs.ts b/apps/dokploy/server/wss/docker-container-logs.ts index 77fba8bb5..c3f902475 100644 --- a/apps/dokploy/server/wss/docker-container-logs.ts +++ b/apps/dokploy/server/wss/docker-container-logs.ts @@ -3,7 +3,7 @@ import { findServerById, IS_CLOUD, validateRequest } from "@dokploy/server"; import { spawn } from "node-pty"; import { Client } from "ssh2"; import { WebSocketServer } from "ws"; -import { getShell } from "./utils"; +import { getShell, isValidContainerId } from "./utils"; export const setupDockerContainerLogsWebSocketServer = ( server: http.Server, @@ -42,6 +42,12 @@ export const setupDockerContainerLogsWebSocketServer = ( return; } + // Security: Validate containerId to prevent command injection + if (!isValidContainerId(containerId)) { + ws.close(4000, "Invalid container ID format"); + return; + } + if (!user || !session) { ws.close(); return; From 5967f48c6b4a225a32d979c8e4b701b4648472d8 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 27 Jan 2026 09:56:28 -0600 Subject: [PATCH 40/40] feat(wss): add directory validation for WebSocket server log paths --- apps/dokploy/server/wss/listen-deployment.ts | 6 ++++++ apps/dokploy/server/wss/utils.ts | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/apps/dokploy/server/wss/listen-deployment.ts b/apps/dokploy/server/wss/listen-deployment.ts index 75ddf7d1d..8aeee2410 100644 --- a/apps/dokploy/server/wss/listen-deployment.ts +++ b/apps/dokploy/server/wss/listen-deployment.ts @@ -3,6 +3,7 @@ import type http from "node:http"; import { findServerById, IS_CLOUD, validateRequest } from "@dokploy/server"; import { Client } from "ssh2"; import { WebSocketServer } from "ws"; +import { readValidDirectory } from "./utils"; export const setupDeploymentLogsWebSocketServer = ( server: http.Server, @@ -40,6 +41,11 @@ export const setupDeploymentLogsWebSocketServer = ( return; } + if (!readValidDirectory(logPath)) { + ws.close(4000, "Invalid log path"); + return; + } + if (!user || !session) { ws.close(); return; diff --git a/apps/dokploy/server/wss/utils.ts b/apps/dokploy/server/wss/utils.ts index be2197501..c749fbc51 100644 --- a/apps/dokploy/server/wss/utils.ts +++ b/apps/dokploy/server/wss/utils.ts @@ -32,6 +32,17 @@ export const isValidShell = (shell: string): boolean => { return allowedShells.includes(shell); }; +export const readValidDirectory = (directory: string) => { + const { BASE_PATH } = paths(); + + const resolvedBase = path.resolve(BASE_PATH); + const resolvedDir = path.resolve(directory); + + return ( + resolvedDir === resolvedBase || + resolvedDir.startsWith(resolvedBase + path.sep) + ); +}; export const getShell = () => { if (IS_CLOUD) { return "NO_AVAILABLE";