Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 216 additions & 0 deletions apps/frontend/src/main/ipc-handlers/dokploy-handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { ipcMain } from 'electron';
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
import path from 'path';
import { IPC_CHANNELS } from '../../shared/constants';
import type { DokployApiRequest, DokployApiResponse, AppSettings, DokployAccount, DokployProjectDeployment } from '../../shared/types';
import { readSettingsFile } from '../settings-utils';
import { parseEnvFile } from './utils';

const DEPLOYMENT_FILE = '.dokploy.json';

/**
* Make a request to the Dokploy API
*/
async function makeDokployRequest<T>(
baseUrl: string,
apiKey: string,
endpoint: string,
method: 'GET' | 'POST',
body?: Record<string, unknown>,
query?: Record<string, string>
): Promise<DokployApiResponse<T>> {
try {
// Build URL - Dokploy uses TRPC format
// baseUrl should be like https://dokploy.example.com/api
let url = `${baseUrl}/${endpoint}`;

// For GET requests with TRPC, we need to pass input as JSON in query string
if (method === 'GET') {
const input = query && Object.keys(query).length > 0 ? query : {};
url += `?input=${encodeURIComponent(JSON.stringify(input))}`;
}

const options: RequestInit = {
method,
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey
}
};

if (method === 'POST' && body) {
options.body = JSON.stringify(body);
}

const response = await fetch(url, options);

if (!response.ok) {
const errorText = await response.text();
return {
success: false,
error: `API error: ${response.status} - ${errorText}`
};
}
Comment on lines +45 to +53
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add a timeout to the fetch request to prevent indefinite hangs.

The fetch call has no timeout configured. If the Dokploy server is unresponsive, the request will hang indefinitely, potentially blocking the UI or causing resource exhaustion.

πŸ”Ž Proposed fix using AbortController
+const DOKPLOY_REQUEST_TIMEOUT_MS = 30000; // 30 seconds

 async function makeDokployRequest<T>(
   baseUrl: string,
   apiKey: string,
   endpoint: string,
   method: 'GET' | 'POST',
   body?: Record<string, unknown>,
   query?: Record<string, string>
 ): Promise<DokployApiResponse<T>> {
   try {
     // Build URL - Dokploy uses TRPC format
     // baseUrl should be like https://dokploy.example.com/api
     let url = `${baseUrl}/${endpoint}`;

     // For GET requests with TRPC, we need to pass input as JSON in query string
     if (method === 'GET') {
       const input = query && Object.keys(query).length > 0 ? query : {};
       url += `?input=${encodeURIComponent(JSON.stringify(input))}`;
     }

+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), DOKPLOY_REQUEST_TIMEOUT_MS);

     const options: RequestInit = {
       method,
       headers: {
         'Content-Type': 'application/json',
         'x-api-key': apiKey
-      }
+      },
+      signal: controller.signal
     };

     if (method === 'POST' && body) {
       options.body = JSON.stringify(body);
     }

     const response = await fetch(url, options);
+    clearTimeout(timeoutId);
πŸ€– Prompt for AI Agents
In apps/frontend/src/main/ipc-handlers/dokploy-handlers.ts around lines 45-53,
the fetch call lacks a timeout and can hang indefinitely; wrap the request with
an AbortController: create a controller, pass controller.signal to fetch, start
a setTimeout that calls controller.abort() after a chosen timeout (e.g., 10s),
and clear the timeout when fetch completes. Catch the abort error and return a
structured { success: false, error: 'Request timed out' } (or include the
abort/error message) while preserving existing API error handling for non-ok
responses. Ensure no memory leak by clearing the timer in all code paths.


const responseData = await response.json();
// TRPC returns data in { result: { data: ... } } format for queries
// or just the data directly for mutations
const data = responseData?.result?.data ?? responseData;
return {
success: true,
data: data as T
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Request failed'
};
}
}

/**
* Read environment variables from env files at a given path
* Checks multiple common env file names
*/
function readEnvFile(servicePath: string): Record<string, string> {
console.log('[Dokploy Backend] readEnvFile called with:', servicePath);

// Common env file names to check (in priority order)
const envFileNames = ['.env', '.env.local', '.env.development', '.env.production'];
let allEnvVars: Record<string, string> = {};

for (const fileName of envFileNames) {
const envPath = path.join(servicePath, fileName);
console.log('[Dokploy Backend] Checking:', envPath, 'exists:', existsSync(envPath));

if (existsSync(envPath)) {
try {
const content = readFileSync(envPath, 'utf-8');
console.log(`[Dokploy Backend] Found ${fileName}, length:`, content.length);
const parsed = parseEnvFile(content);
console.log(`[Dokploy Backend] Parsed from ${fileName}:`, Object.keys(parsed));
// Merge (earlier files take precedence)
allEnvVars = { ...parsed, ...allEnvVars };
} catch (err) {
console.error(`[Dokploy Backend] Error reading ${fileName}:`, err);
}
}
}

console.log('[Dokploy Backend] Total env vars found:', Object.keys(allEnvVars));
return allEnvVars;
Comment on lines +76 to +101
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | πŸ”΅ Trivial

Remove or conditionally enable verbose logging in production.

The console.log statements throughout readEnvFile are useful for debugging but will clutter production logs. Consider using a debug flag or removing them.

πŸ”Ž Suggested approach
+const DEBUG_DOKPLOY = process.env.NODE_ENV === 'development';

 function readEnvFile(servicePath: string): Record<string, string> {
-  console.log('[Dokploy Backend] readEnvFile called with:', servicePath);
+  if (DEBUG_DOKPLOY) console.log('[Dokploy Backend] readEnvFile called with:', servicePath);

   // Common env file names to check (in priority order)
   const envFileNames = ['.env', '.env.local', '.env.development', '.env.production'];
   let allEnvVars: Record<string, string> = {};

   for (const fileName of envFileNames) {
     const envPath = path.join(servicePath, fileName);
-    console.log('[Dokploy Backend] Checking:', envPath, 'exists:', existsSync(envPath));
+    if (DEBUG_DOKPLOY) console.log('[Dokploy Backend] Checking:', envPath, 'exists:', existsSync(envPath));
     // ... apply similar changes to other log statements
   }

Committable suggestion skipped: line range outside the PR's diff.

πŸ€– Prompt for AI Agents
In apps/frontend/src/main/ipc-handlers/dokploy-handlers.ts around lines 76 to
101, the function readEnvFile contains multiple console.log/console.error calls
that should not run unconditionally in production; remove the noisy console
statements or wrap them behind a configurable debug flag or environment check
(e.g., process.env.DEBUG or an app logger.isDebugEnabled()) and replace direct
console usage with the app's logger where available so that logging is only
emitted when debugging is enabled and errors still get logged appropriately in
production.

}

/**
* Register Dokploy-related IPC handlers
*/
export function registerDokployHandlers(): void {
// Main Dokploy API handler
ipcMain.handle(
IPC_CHANNELS.DOKPLOY_API,
async (_, request: DokployApiRequest): Promise<DokployApiResponse<unknown>> => {
try {
// Get settings to find the Dokploy account
const settings = readSettingsFile() as unknown as AppSettings;
const dokployAccounts: DokployAccount[] = settings?.deploymentProviders?.dokploy || [];

// Find the account by ID
const account = dokployAccounts.find((a: DokployAccount) => a.id === request.accountId);
if (!account) {
return {
success: false,
error: 'Dokploy account not found'
};
}

// Make the API request
return await makeDokployRequest(
account.baseUrl,
account.apiKey,
request.endpoint,
request.method,
request.body,
request.query
);
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
);

// Read env variables from a service path
ipcMain.handle(
IPC_CHANNELS.DOKPLOY_READ_ENV,
async (_, servicePath: string): Promise<DokployApiResponse<Record<string, string>>> => {
try {
const envVars = readEnvFile(servicePath);
return { success: true, data: envVars };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to read env file'
};
}
}
);

// Save deployment info to project directory
ipcMain.handle(
IPC_CHANNELS.DOKPLOY_SAVE_DEPLOYMENT,
async (_, projectPath: string, deployment: DokployProjectDeployment): Promise<DokployApiResponse<void>> => {
try {
const filePath = path.join(projectPath, DEPLOYMENT_FILE);
writeFileSync(filePath, JSON.stringify(deployment, null, 2), 'utf-8');
return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to save deployment info'
};
}
}
);

// Get deployment info from project directory
ipcMain.handle(
IPC_CHANNELS.DOKPLOY_GET_DEPLOYMENT,
async (_, projectPath: string): Promise<DokployApiResponse<DokployProjectDeployment | null>> => {
try {
const filePath = path.join(projectPath, DEPLOYMENT_FILE);
if (!existsSync(filePath)) {
return { success: true, data: null };
}
const content = readFileSync(filePath, 'utf-8');
const deployment = JSON.parse(content) as DokployProjectDeployment;
return { success: true, data: deployment };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to read deployment info'
};
}
}
);
Comment on lines +177 to +196
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | πŸ”΅ Trivial

Handle JSON parse errors gracefully for corrupted deployment files.

If .dokploy.json contains malformed JSON, JSON.parse will throw and the error handler will return a generic message. Consider providing a more specific error message for parse failures.

πŸ”Ž Proposed improvement
   ipcMain.handle(
     IPC_CHANNELS.DOKPLOY_GET_DEPLOYMENT,
     async (_, projectPath: string): Promise<DokployApiResponse<DokployProjectDeployment | null>> => {
       try {
         const filePath = path.join(projectPath, DEPLOYMENT_FILE);
         if (!existsSync(filePath)) {
           return { success: true, data: null };
         }
         const content = readFileSync(filePath, 'utf-8');
-        const deployment = JSON.parse(content) as DokployProjectDeployment;
-        return { success: true, data: deployment };
+        try {
+          const deployment = JSON.parse(content) as DokployProjectDeployment;
+          return { success: true, data: deployment };
+        } catch (parseError) {
+          return {
+            success: false,
+            error: `Invalid deployment file format: ${parseError instanceof Error ? parseError.message : 'parse error'}`
+          };
+        }
       } catch (error) {
         return {
           success: false,
           error: error instanceof Error ? error.message : 'Failed to read deployment info'
         };
       }
     }
   );
πŸ€– Prompt for AI Agents
In apps/frontend/src/main/ipc-handlers/dokploy-handlers.ts around lines 177 to
196, the code treats any exception from reading/parsing .dokploy.json the same
and returns a generic error message; detect JSON parse failures specifically and
return a clearer message. Change the try/catch so that after reading the file
you catch JSON.parse errors (e.g. detect instanceof SyntaxError or inspect the
error message) and return success: false with a descriptive message like
"Malformed .dokploy.json: failed to parse JSON" (including filePath for
context), while preserving the existing generic error branch for other I/O
errors.


// Delete deployment info from project directory
ipcMain.handle(
IPC_CHANNELS.DOKPLOY_DELETE_DEPLOYMENT,
async (_, projectPath: string): Promise<DokployApiResponse<void>> => {
try {
const filePath = path.join(projectPath, DEPLOYMENT_FILE);
if (existsSync(filePath)) {
unlinkSync(filePath);
}
return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to delete deployment info'
};
}
}
);
}
7 changes: 6 additions & 1 deletion apps/frontend/src/main/ipc-handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { registerChangelogHandlers } from './changelog-handlers';
import { registerInsightsHandlers } from './insights-handlers';
import { registerMemoryHandlers } from './memory-handlers';
import { registerAppUpdateHandlers } from './app-update-handlers';
import { registerDokployHandlers } from './dokploy-handlers';
import { notificationService } from '../notification-service';

/**
Expand Down Expand Up @@ -98,6 +99,9 @@ export function setupIpcHandlers(
// App auto-update handlers
registerAppUpdateHandlers();

// Dokploy deployment handlers
registerDokployHandlers();

console.warn('[IPC] All handler modules registered successfully');
}

Expand All @@ -119,5 +123,6 @@ export {
registerChangelogHandlers,
registerInsightsHandlers,
registerMemoryHandlers,
registerAppUpdateHandlers
registerAppUpdateHandlers,
registerDokployHandlers
};
11 changes: 8 additions & 3 deletions apps/frontend/src/preload/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { IdeationAPI, createIdeationAPI } from './modules/ideation-api';
import { InsightsAPI, createInsightsAPI } from './modules/insights-api';
import { AppUpdateAPI, createAppUpdateAPI } from './app-update-api';
import { GitHubAPI, createGitHubAPI } from './modules/github-api';
import { DokployAPI, createDokployAPI } from './modules/dokploy-api';

export interface ElectronAPI extends
ProjectAPI,
Expand All @@ -18,7 +19,8 @@ export interface ElectronAPI extends
AgentAPI,
IdeationAPI,
InsightsAPI,
AppUpdateAPI {
AppUpdateAPI,
DokployAPI {
github: GitHubAPI;
}

Expand All @@ -32,6 +34,7 @@ export const createElectronAPI = (): ElectronAPI => ({
...createIdeationAPI(),
...createInsightsAPI(),
...createAppUpdateAPI(),
...createDokployAPI(),
github: createGitHubAPI()
});

Expand All @@ -46,7 +49,8 @@ export {
createIdeationAPI,
createInsightsAPI,
createAppUpdateAPI,
createGitHubAPI
createGitHubAPI,
createDokployAPI
};

export type {
Expand All @@ -59,5 +63,6 @@ export type {
IdeationAPI,
InsightsAPI,
AppUpdateAPI,
GitHubAPI
GitHubAPI,
DokployAPI
};
24 changes: 24 additions & 0 deletions apps/frontend/src/preload/api/modules/dokploy-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ipcRenderer } from 'electron';
import { IPC_CHANNELS } from '../../../shared/constants';
import type { DokployApiRequest, DokployApiResponse, DokployProjectDeployment } from '../../../shared/types';

export interface DokployAPI {
dokployApi: <T = unknown>(request: DokployApiRequest) => Promise<DokployApiResponse<T>>;
dokployReadEnv: (servicePath: string) => Promise<DokployApiResponse<Record<string, string>>>;
dokploySaveDeployment: (projectPath: string, deployment: DokployProjectDeployment) => Promise<DokployApiResponse<void>>;
dokployGetDeployment: (projectPath: string) => Promise<DokployApiResponse<DokployProjectDeployment | null>>;
dokployDeleteDeployment: (projectPath: string) => Promise<DokployApiResponse<void>>;
}

export const createDokployAPI = (): DokployAPI => ({
dokployApi: <T = unknown>(request: DokployApiRequest): Promise<DokployApiResponse<T>> =>
ipcRenderer.invoke(IPC_CHANNELS.DOKPLOY_API, request),
dokployReadEnv: (servicePath: string): Promise<DokployApiResponse<Record<string, string>>> =>
ipcRenderer.invoke(IPC_CHANNELS.DOKPLOY_READ_ENV, servicePath),
dokploySaveDeployment: (projectPath: string, deployment: DokployProjectDeployment): Promise<DokployApiResponse<void>> =>
ipcRenderer.invoke(IPC_CHANNELS.DOKPLOY_SAVE_DEPLOYMENT, projectPath, deployment),
dokployGetDeployment: (projectPath: string): Promise<DokployApiResponse<DokployProjectDeployment | null>> =>
ipcRenderer.invoke(IPC_CHANNELS.DOKPLOY_GET_DEPLOYMENT, projectPath),
dokployDeleteDeployment: (projectPath: string): Promise<DokployApiResponse<void>> =>
ipcRenderer.invoke(IPC_CHANNELS.DOKPLOY_DELETE_DEPLOYMENT, projectPath)
});
10 changes: 10 additions & 0 deletions apps/frontend/src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { GitHubIssues } from './components/GitHubIssues';
import { GitHubPRs } from './components/github-prs';
import { Changelog } from './components/Changelog';
import { Worktrees } from './components/Worktrees';
import { Deploy } from './components/Deploy';
import { WelcomeScreen } from './components/WelcomeScreen';
import { RateLimitModal } from './components/RateLimitModal';
import { SDKRateLimitModal } from './components/SDKRateLimitModal';
Expand Down Expand Up @@ -687,6 +688,15 @@ export function App() {
{activeView === 'worktrees' && (activeProjectId || selectedProjectId) && (
<Worktrees projectId={activeProjectId || selectedProjectId!} />
)}
{activeView === 'deploy' && (activeProjectId || selectedProjectId) && (
<Deploy
projectId={activeProjectId || selectedProjectId!}
onOpenSettings={() => {
setSettingsInitialSection('deployment');
setIsSettingsDialogOpen(true);
}}
/>
)}
{activeView === 'agent-tools' && (
<div className="flex h-full items-center justify-center">
<div className="text-center">
Expand Down
Loading