From 090c0226eda8e1c164af561ed3f21a581f26d34e Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Thu, 9 Apr 2026 09:27:31 -0600 Subject: [PATCH 1/6] feat: add AI log analysis component and integrate into deployment views - Introduced the AnalyzeLogs component for analyzing logs using AI, allowing users to select AI providers and view analysis results. - Integrated AnalyzeLogs into the ShowDeployment and DockerLogsId components, enabling log analysis for both build and runtime contexts. - Updated the AI router to include a new endpoint for log analysis, which processes logs and returns structured insights. - Enhanced the AI provider selection logic to support new providers, including Z.AI and MiniMax. This feature enhances the user experience by providing actionable insights from logs, improving troubleshooting and operational efficiency. --- .../deployments/show-deployment.tsx | 2 + .../dashboard/docker/logs/analyze-logs.tsx | 170 ++++++++++ .../dashboard/docker/logs/docker-logs-id.tsx | 2 + .../project/ai/template-generator.tsx | 14 +- .../dashboard/settings/handle-ai.tsx | 319 ++++++++++++------ apps/dokploy/server/api/routers/ai.ts | 116 +++++++ packages/server/src/services/ai.ts | 238 ++++++------- .../server/src/utils/ai/select-ai-provider.ts | 27 ++ 8 files changed, 651 insertions(+), 237 deletions(-) create mode 100644 apps/dokploy/components/dashboard/docker/logs/analyze-logs.tsx diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx index 4285f04c4..9f078f9d2 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx @@ -1,6 +1,7 @@ import copy from "copy-to-clipboard"; import { Check, Copy, Loader2 } from "lucide-react"; import { useEffect, useRef, useState } from "react"; +import { AnalyzeLogs } from "@/components/dashboard/docker/logs/analyze-logs"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; @@ -165,6 +166,7 @@ export const ShowDeployment = ({ )} + {serverId && (
diff --git a/apps/dokploy/components/dashboard/docker/logs/analyze-logs.tsx b/apps/dokploy/components/dashboard/docker/logs/analyze-logs.tsx new file mode 100644 index 000000000..f330c3fe2 --- /dev/null +++ b/apps/dokploy/components/dashboard/docker/logs/analyze-logs.tsx @@ -0,0 +1,170 @@ +"use client"; +import { Bot, Loader2, RotateCcw, X } from "lucide-react"; +import { useState } from "react"; +import ReactMarkdown from "react-markdown"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { api } from "@/utils/api"; +import type { LogLine } from "./utils"; + +interface Props { + logs: LogLine[]; + context: "build" | "runtime"; +} + +const MAX_LOG_LINES = 200; + +export function AnalyzeLogs({ logs, context }: Props) { + const [open, setOpen] = useState(false); + const [aiId, setAiId] = useState(""); + const { data: providers } = api.ai.getEnabledProviders.useQuery(undefined, { + enabled: open, + }); + const { mutate, isPending, data, reset } = + api.ai.analyzeLogs.useMutation({ + onError: (error) => { + toast.error("Analysis failed", { + description: error.message, + }); + }, + }); + + const handleAnalyze = () => { + if (!aiId || logs.length === 0) return; + + const logsText = logs + .slice(-MAX_LOG_LINES) + .map((l) => l.message) + .join("\n"); + + mutate({ aiId, logs: logsText, context }); + }; + + return ( + { + setOpen(isOpen); + if (!isOpen) { + reset(); + setAiId(""); + } + }} + > + + + + +
+
+ + Log Analysis +
+ +
+
+ {!data?.analysis ? ( + <> + + + + ) : ( + <> +
+
+ {data.analysis} +
+
+
+ + +
+ + )} +
+
+
+ ); +} diff --git a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx index 59b939008..ace20b343 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -12,6 +12,7 @@ import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; +import { AnalyzeLogs } from "./analyze-logs"; import { LineCountFilter } from "./line-count-filter"; import { SinceLogsFilter, type TimeFilter } from "./since-logs-filter"; import { StatusLogsFilter } from "./status-logs-filter"; @@ -377,6 +378,7 @@ export const DockerLogsId: React.FC = ({ Download logs +
{isPaused && ( diff --git a/apps/dokploy/components/dashboard/project/ai/template-generator.tsx b/apps/dokploy/components/dashboard/project/ai/template-generator.tsx index 76a491aa0..64d4b0776 100644 --- a/apps/dokploy/components/dashboard/project/ai/template-generator.tsx +++ b/apps/dokploy/components/dashboard/project/ai/template-generator.tsx @@ -298,7 +298,19 @@ export const TemplateGenerator = ({ environmentId }: Props) => {
+ + + - {field.value - ? (selectedModel?.id ?? field.value) - : "Select a model"} - - - - - - - + + + + {modelSearch ? ( + + ) : ( + "No models found." + )} + + {displayModels.map((model) => { + const isSelected = field.value === model.id; + return ( + { + field.onChange(model.id); + setModelPopoverOpen(false); + setModelSearch(""); + }} + > + + {model.id} + + ); + })} + + + + + ) : ( + + - - No models found. - {displayModels.map((model) => { - const isSelected = field.value === model.id; - return ( - { - field.onChange(model.id); - setModelPopoverOpen(false); - setModelSearch(""); - }} - > - - {model.id} - - ); - })} - - - - - - Select an AI model to use - - - - ); - }} - /> - )} + + )} +
+
+ + Select a model from the list or type a custom model name + + + + ); + }} + /> { )} /> -
+
+ @@ -383,3 +475,42 @@ export const HandleAi = ({ aiId }: Props) => { ); }; + +function TestConnectionButton({ + apiUrl, + apiKey, + model, +}: { + apiUrl: string; + apiKey: string; + model: string; +}) { + const { mutate, isPending } = api.ai.testConnection.useMutation({ + onSuccess: () => { + toast.success("Connection successful"); + }, + onError: (error) => { + toast.error("Connection failed", { + description: error.message, + }); + }, + }); + + const isDisabled = !apiUrl || !model; + + return ( + + ); +} diff --git a/apps/dokploy/server/api/routers/ai.ts b/apps/dokploy/server/api/routers/ai.ts index a4527497d..139227221 100644 --- a/apps/dokploy/server/api/routers/ai.ts +++ b/apps/dokploy/server/api/routers/ai.ts @@ -25,9 +25,11 @@ import { findProjectById } from "@dokploy/server/services/project"; import { getProviderHeaders, getProviderName, + selectAIProvider, type Model, } from "@dokploy/server/utils/ai/select-ai-provider"; import { TRPCError } from "@trpc/server"; +import { generateText } from "ai"; import { z } from "zod"; import { slugify } from "@/lib/slug"; import { @@ -95,6 +97,30 @@ export const aiRouter = createTRPCRouter({ owned_by: "perplexity", }, ] as Model[]; + case "zai": + return [ + { + id: "glm-5", + object: "model", + created: Date.now(), + owned_by: "zai", + }, + { + id: "glm-4.7", + object: "model", + created: Date.now(), + owned_by: "zai", + }, + ] as Model[]; + case "minimax": + return [ + { + id: "MiniMax-M2.7", + object: "model", + created: Date.now(), + owned_by: "minimax", + }, + ] as Model[]; default: if (!input.apiKey) throw new TRPCError({ @@ -174,6 +200,96 @@ export const aiRouter = createTRPCRouter({ return await deleteAiSettings(input.aiId); }), + getEnabledProviders: protectedProcedure.query(async ({ ctx }) => { + const settings = await getAiSettingsByOrganizationId( + ctx.session.activeOrganizationId, + ); + return settings + .filter((s) => s.isEnabled) + .map((s) => ({ aiId: s.aiId, name: s.name, model: s.model })); + }), + + analyzeLogs: protectedProcedure + .input( + z.object({ + aiId: z.string().min(1), + logs: z.string().min(1), + context: z.enum(["build", "runtime"]), + }), + ) + .mutation(async ({ input }) => { + try { + const aiSettings = await getAiSettingById(input.aiId); + if (!aiSettings?.isEnabled) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "AI provider is not enabled", + }); + } + + const provider = selectAIProvider(aiSettings); + const model = provider(aiSettings.model); + + const contextLabel = + input.context === "build" ? "build/deployment" : "runtime/container"; + + const result = await generateText({ + model, + prompt: `You are a DevOps engineer analyzing ${contextLabel} logs. Analyze the following logs and provide: + +1. **Summary**: A brief summary of what's happening +2. **Issues Found**: Any errors, warnings, or problems detected +3. **Root Cause**: The most likely root cause if there are errors +4. **Suggested Fix**: Actionable steps to resolve the issues + +Be concise and practical. Focus on the most important issues. If the logs look healthy, say so briefly. + +Logs: +${input.logs}`, + }); + + return { analysis: result.text }; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + error instanceof Error ? error.message : `Analysis failed: ${error}`, + }); + } + }), + + testConnection: protectedProcedure + .input( + z.object({ + apiUrl: z.string().min(1), + apiKey: z.string(), + model: z.string().min(1), + }), + ) + .mutation(async ({ input }) => { + try { + const provider = selectAIProvider({ + apiUrl: input.apiUrl, + apiKey: input.apiKey, + }); + const model = provider(input.model); + const result = await generateText({ + model, + prompt: "Reply with 'ok'", + }); + if (!result.text) { + throw new Error("No response received from the model"); + } + return { success: true, message: "Connection successful" }; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + error instanceof Error ? error.message : `Connection failed: ${error}`, + }); + } + }), + suggest: protectedProcedure .input( z.object({ diff --git a/packages/server/src/services/ai.ts b/packages/server/src/services/ai.ts index ccfb31fc1..6e90d82d0 100644 --- a/packages/server/src/services/ai.ts +++ b/packages/server/src/services/ai.ts @@ -108,22 +108,45 @@ export const suggestVariants = async ({ ip = "127.0.0.1"; } - const suggestionsSchema = z.object({ + const fullSchema = z.object({ suggestions: z.array( z.object({ id: z.string(), name: z.string(), shortDescription: z.string(), description: z.string(), + dockerCompose: z.string(), + envVariables: z.array( + z.object({ + name: z.string(), + value: z.string(), + }), + ), + domains: z.array( + z.object({ + host: z.string(), + port: z.number(), + serviceName: z.string(), + }), + ), + configFiles: z + .array( + z.object({ + content: z.string(), + filePath: z.string(), + }), + ) + .optional(), }), ), }); - const suggestionsResult = await generateText({ + + const result = await generateText({ model, // @ts-ignore - Zod + AI SDK Output.object() causes excessively deep instantiation - output: Output.object({ schema: suggestionsSchema }), + output: Output.object({ schema: fullSchema }), prompt: ` - Act as advanced DevOps engineer and analyze the user's request to determine the appropriate suggestions (up to 3 items). + Act as advanced DevOps engineer. Analyze the user's request and generate up to 3 deployment suggestions, each with a complete docker compose configuration. CRITICAL - Read the user's request carefully and follow the appropriate strategy: @@ -139,163 +162,94 @@ export const suggestVariants = async ({ - Example: For "personal blog" → "WordPress", "Ghost", "Hugo with Nginx" - The name should be the actual project name - Return your response as a JSON object with the following structure: + Return your response as a JSON object with this structure: { "suggestions": [ { "id": "project-or-variant-slug", "name": "Project Name or Variant Name", "shortDescription": "Brief one-line description", - "description": "Detailed description" + "description": "Detailed description of the project/variant", + "dockerCompose": "yaml string here", + "envVariables": [{"name": "VAR_NAME", "value": "example_value"}], + "domains": [{"host": "domain.com", "port": 3000, "serviceName": "service"}], + "configFiles": [{"content": "file content", "filePath": "path/to/file"}] } ] } - Important rules for the response: + Suggestion Rules: 1. Use slug format for the id field (lowercase, hyphenated) - 2. Determine which strategy to use based on whether the user specified a particular application or described a general need - 3. For Strategy A (specific app): The name must include the app name and describe the variant configuration - 4. For Strategy B (general need): The name should be the actual project/tool name that fulfills the need - 5. The description field should ONLY contain a plain text description of the project or variant, its features, and use cases - 6. Do NOT include any code snippets, configuration examples, or installation instructions in the description - 7. The shortDescription should be a single-line summary focusing on key technologies or differentiators - 8. All suggestions should be installable in docker and have docker compose support - 9. Provide variety in your suggestions - different complexity levels, tech stacks, or approaches + 2. The description field should ONLY contain plain text — no code snippets or installation instructions + 3. The shortDescription should be a single-line summary focusing on key technologies or differentiators + 4. All suggestions should be installable in docker and have docker compose support + 5. Provide variety in your suggestions - different complexity levels, tech stacks, or approaches - User wants to create a new project with the following details: + Docker Compose Rules: + 1. Use placeholder like \${VARIABLE_NAME-default} for generated variables in the docker-compose.yml + 2. Use complex values for passwords/secrets variables + 3. Don't set container_name field in services + 4. Don't set version field in the docker compose + 5. Don't set ports like 'ports: 3000:3000', use 'ports: "3000"' instead + 6. If a service depends on a database or other service, INCLUDE that service in the docker-compose + 7. Make sure all required services are defined in the docker-compose - ${input} + Docker Image Rules (CRITICAL): + 1. ALWAYS use 'image:' field, NEVER use 'build:' field + 2. NEVER use 'build: .' or any build directive - we don't have local Dockerfiles + 3. Use images from Docker Hub or other public registries (e.g., docker.io, ghcr.io, quay.io) + 4. For dependencies (databases, redis, etc.), use official images (e.g., postgres:16, redis:7, etc.) + 5. Always specify image tags - avoid using 'latest' tag, use specific versions when possible + 6. Examples of correct image usage: + - image: sendingtk/chatwoot:develop + - image: postgres:16-alpine + - image: redis:7-alpine + 7. Examples of INCORRECT usage (DO NOT USE): + - build: . + - build: ./app + - build: + context: . + dockerfile: Dockerfile + + Volume Mounting and Configuration Rules: + 1. DO NOT create configuration files unless the service CANNOT work without them + 2. Most services can work with just environment variables - USE THEM FIRST + 3. If and ONLY IF a config file is absolutely required: + - Keep it minimal with only critical settings + - Use "../files/" prefix for all mounts + - Format: "../files/folder:/container/path" + 4. DO NOT add configuration files for default configs, env-configurable settings, or proxy/routing configs + + Environment Variables Rules: + 1. For the envVariables array, provide ACTUAL example values, not placeholders + 2. Use realistic example values (e.g., "admin@example.com" for emails, "mypassword123" for passwords) + 3. DO NOT use \${VARIABLE_NAME-default} syntax in the envVariables values + 4. ONLY include environment variables that are actually used in the docker-compose + 5. Every environment variable referenced in the docker-compose MUST have a corresponding entry in envVariables + + Domain Rules - For each service that needs to be exposed to the internet: + 1. Define a domain with: + - host: {service-name}-{random-3-chars-hex}-${ip ? ip.replaceAll(".", "-") : ""}.traefik.me + - port: the internal port the service runs on + - serviceName: the name of the service in the docker-compose + 2. Make sure the service is properly configured to work with the specified port + + User's request: ${input} `, }); - const object = suggestionsResult.output as SuggestionsOutput | undefined; - if (object?.suggestions?.length) { - const dockerSchema = z.object({ - dockerCompose: z.string(), - envVariables: z.array( - z.object({ - name: z.string(), - value: z.string(), - }), - ), - domains: z.array( - z.object({ - host: z.string(), - port: z.number(), - serviceName: z.string(), - }), - ), - configFiles: z - .array( - z.object({ - content: z.string(), - filePath: z.string(), - }), - ) - .optional(), + const output = result.output as + | { suggestions: (SuggestionItem & DockerOutput)[] } + | undefined; + + if (!output?.suggestions?.length) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "No suggestions found", }); - const result = []; - for (const suggestion of object.suggestions) { - try { - const dockerResult = await generateText({ - model, - // @ts-ignore - Zod + AI SDK Output.object() causes excessively deep instantiation - output: Output.object({ schema: dockerSchema }), - prompt: ` - Act as advanced DevOps engineer and generate docker compose with environment variables and domain configurations needed to install the following project. - - Return your response as a JSON object with this structure: - { - "dockerCompose": "yaml string here", - "envVariables": [{"name": "VAR_NAME", "value": "example_value"}], - "domains": [{"host": "domain.com", "port": 3000, "serviceName": "service"}], - "configFiles": [{"content": "file content", "filePath": "path/to/file"}] - } - - Note: configFiles is optional - only include it if configuration files are absolutely required. - - Follow these rules: - - Docker Compose Rules: - 1. Use placeholder like \${VARIABLE_NAME-default} for generated variables in the docker-compose.yml - 2. Use complex values for passwords/secrets variables - 3. Don't set container_name field in services - 4. Don't set version field in the docker compose - 5. Don't set ports like 'ports: 3000:3000', use 'ports: "3000"' instead - 6. If a service depends on a database or other service, INCLUDE that service in the docker-compose - 7. Make sure all required services are defined in the docker-compose - - Docker Image Rules (CRITICAL): - 1. ALWAYS use 'image:' field, NEVER use 'build:' field - 2. NEVER use 'build: .' or any build directive - we don't have local Dockerfiles - 3. Use images from Docker Hub or other public registries (e.g., docker.io, ghcr.io, quay.io) - 4. For dependencies (databases, redis, etc.), use official images (e.g., postgres:16, redis:7, etc.) - 5. Always specify image tags - avoid using 'latest' tag, use specific versions when possible - 6. Examples of correct image usage: - - image: sendingtk/chatwoot:develop - - image: postgres:16-alpine - - image: redis:7-alpine - - image: chatwoot/chatwoot:latest - 7. Examples of INCORRECT usage (DO NOT USE): - - build: . - - build: ./app - - build: - context: . - dockerfile: Dockerfile - - Volume Mounting and Configuration Rules: - 1. DO NOT create configuration files unless the service CANNOT work without them - 2. Most services can work with just environment variables - USE THEM FIRST - 3. Ask yourself: "Can this be configured with an environment variable instead?" - 4. If and ONLY IF a config file is absolutely required: - - Keep it minimal with only critical settings - - Use "../files/" prefix for all mounts - - Format: "../files/folder:/container/path" - 5. DO NOT add configuration files for: - - Default configurations that work out of the box - - Settings that can be handled by environment variables - - Proxy or routing configurations (these are handled elsewhere) - - Environment Variables Rules: - 1. For the envVariables array, provide ACTUAL example values, not placeholders - 2. Use realistic example values (e.g., "admin@example.com" for emails, "mypassword123" for passwords) - 3. DO NOT use \${VARIABLE_NAME-default} syntax in the envVariables values - 4. ONLY include environment variables that are actually used in the docker-compose - 5. Every environment variable referenced in the docker-compose MUST have a corresponding entry in envVariables - 6. Do not include environment variables for services that don't exist in the docker-compose - - For each service that needs to be exposed to the internet: - 1. Define a domain configuration with: - - host: the domain name for the service in format: {service-name}-{random-3-chars-hex}-${ip ? ip.replaceAll(".", "-") : ""}.traefik.me - - port: the internal port the service runs on - - serviceName: the name of the service in the docker-compose - 2. Make sure the service is properly configured to work with the specified port - - User's original request: ${input} - - Project details: - ${suggestion?.description} - `, - }); - const docker = dockerResult.output as DockerOutput | undefined; - if (docker?.dockerCompose) { - result.push({ - ...suggestion, - ...docker, - }); - } - } catch (error) { - console.error("Error in docker compose generation:", error); - } - } - - return result; } - throw new TRPCError({ - code: "NOT_FOUND", - message: "No suggestions found", - }); + return output.suggestions.filter((s) => s.dockerCompose); } catch (error) { console.error("Error in suggestVariants:", error); throw error; diff --git a/packages/server/src/utils/ai/select-ai-provider.ts b/packages/server/src/utils/ai/select-ai-provider.ts index b6477c573..52d7c6224 100644 --- a/packages/server/src/utils/ai/select-ai-provider.ts +++ b/packages/server/src/utils/ai/select-ai-provider.ts @@ -17,6 +17,9 @@ export function getProviderName(apiUrl: string) { if (apiUrl.includes(":11434") || apiUrl.includes("ollama")) return "ollama"; if (apiUrl.includes("api.deepinfra.com")) return "deepinfra"; if (apiUrl.includes("generativelanguage.googleapis.com")) return "gemini"; + if (apiUrl.includes("openrouter.ai")) return "openrouter"; + if (apiUrl.includes("api.z.ai")) return "zai"; + if (apiUrl.includes("api.minimax.io")) return "minimax"; return "custom"; } @@ -87,6 +90,30 @@ export function selectAIProvider(config: { apiUrl: string; apiKey: string }) { Authorization: `Bearer ${config.apiKey}`, }, }); + case "openrouter": + return createOpenAICompatible({ + name: "openrouter", + baseURL: config.apiUrl, + headers: { + Authorization: `Bearer ${config.apiKey}`, + }, + }); + case "zai": + return createOpenAICompatible({ + name: "zai", + baseURL: config.apiUrl, + headers: { + Authorization: `Bearer ${config.apiKey}`, + }, + }); + case "minimax": + return createOpenAICompatible({ + name: "minimax", + baseURL: config.apiUrl, + headers: { + Authorization: `Bearer ${config.apiKey}`, + }, + }); case "custom": return createOpenAICompatible({ name: "custom", From fbde5be02ce317dfe0e99d2b4fef9053c1324693 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:20:44 +0000 Subject: [PATCH 2/6] [autofix.ci] apply automated fixes --- .../dashboard/docker/logs/analyze-logs.tsx | 21 +++++++++++-------- .../dashboard/docker/logs/docker-logs-id.tsx | 2 +- apps/dokploy/server/api/routers/ai.ts | 8 +++++-- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/apps/dokploy/components/dashboard/docker/logs/analyze-logs.tsx b/apps/dokploy/components/dashboard/docker/logs/analyze-logs.tsx index f330c3fe2..c26ce5bfe 100644 --- a/apps/dokploy/components/dashboard/docker/logs/analyze-logs.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/analyze-logs.tsx @@ -32,14 +32,13 @@ export function AnalyzeLogs({ logs, context }: Props) { const { data: providers } = api.ai.getEnabledProviders.useQuery(undefined, { enabled: open, }); - const { mutate, isPending, data, reset } = - api.ai.analyzeLogs.useMutation({ - onError: (error) => { - toast.error("Analysis failed", { - description: error.message, - }); - }, - }); + const { mutate, isPending, data, reset } = api.ai.analyzeLogs.useMutation({ + onError: (error) => { + toast.error("Analysis failed", { + description: error.message, + }); + }, + }); const handleAnalyze = () => { if (!aiId || logs.length === 0) return; @@ -119,7 +118,11 @@ export function AnalyzeLogs({ logs, context }: Props) { ) : ( <> - Analyze {logs.length > MAX_LOG_LINES ? `last ${MAX_LOG_LINES}` : logs.length} lines + Analyze{" "} + {logs.length > MAX_LOG_LINES + ? `last ${MAX_LOG_LINES}` + : logs.length}{" "} + lines )} diff --git a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx index ace20b343..8d8842ac0 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -378,7 +378,7 @@ export const DockerLogsId: React.FC = ({ Download logs - +
{isPaused && ( diff --git a/apps/dokploy/server/api/routers/ai.ts b/apps/dokploy/server/api/routers/ai.ts index 139227221..48bacfcba 100644 --- a/apps/dokploy/server/api/routers/ai.ts +++ b/apps/dokploy/server/api/routers/ai.ts @@ -253,7 +253,9 @@ ${input.logs}`, throw new TRPCError({ code: "BAD_REQUEST", message: - error instanceof Error ? error.message : `Analysis failed: ${error}`, + error instanceof Error + ? error.message + : `Analysis failed: ${error}`, }); } }), @@ -285,7 +287,9 @@ ${input.logs}`, throw new TRPCError({ code: "BAD_REQUEST", message: - error instanceof Error ? error.message : `Connection failed: ${error}`, + error instanceof Error + ? error.message + : `Connection failed: ${error}`, }); } }), From 8d8658a478b0bbe6e2ef796c797f3a0750247639 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Thu, 9 Apr 2026 11:27:19 -0600 Subject: [PATCH 3/6] fix: update Z.AI API URL and enhance AI router access control - Corrected the API URL for Z.AI by removing the trailing slash. - Modified the AI router mutation to include context and added access control to ensure users can only access their organization's AI settings. These changes improve the accuracy of the API integration and enhance security by enforcing organizational access restrictions. --- apps/dokploy/components/dashboard/settings/handle-ai.tsx | 2 +- apps/dokploy/server/api/routers/ai.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/handle-ai.tsx b/apps/dokploy/components/dashboard/settings/handle-ai.tsx index db225bb58..18915609e 100644 --- a/apps/dokploy/components/dashboard/settings/handle-ai.tsx +++ b/apps/dokploy/components/dashboard/settings/handle-ai.tsx @@ -68,7 +68,7 @@ const AI_PROVIDERS = [ { name: "DeepInfra", apiUrl: "https://api.deepinfra.com/v1/openai" }, { name: "Ollama", apiUrl: "http://localhost:11434" }, { name: "OpenRouter", apiUrl: "https://openrouter.ai/api/v1" }, - { name: "Z.AI", apiUrl: "https://api.z.ai/api/paas/v4/" }, + { name: "Z.AI", apiUrl: "https://api.z.ai/api/paas/v4" }, { name: "MiniMax", apiUrl: "https://api.minimax.io/v1" }, ] as const; diff --git a/apps/dokploy/server/api/routers/ai.ts b/apps/dokploy/server/api/routers/ai.ts index 48bacfcba..3a299235a 100644 --- a/apps/dokploy/server/api/routers/ai.ts +++ b/apps/dokploy/server/api/routers/ai.ts @@ -217,7 +217,7 @@ export const aiRouter = createTRPCRouter({ context: z.enum(["build", "runtime"]), }), ) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { try { const aiSettings = await getAiSettingById(input.aiId); if (!aiSettings?.isEnabled) { @@ -227,6 +227,13 @@ export const aiRouter = createTRPCRouter({ }); } + if (aiSettings.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Access denied", + }); + } + const provider = selectAIProvider(aiSettings); const model = provider(aiSettings.model); From 7c10610a5ac175c806c0a8e5904ba1e8145d3198 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Thu, 9 Apr 2026 11:40:02 -0600 Subject: [PATCH 4/6] feat: add readLogs procedure to multiple routers for container log retrieval - Implemented a new `readLogs` procedure across various routers (application, compose, libsql, mariadb, mongo, mysql, postgres, redis) to enable users to retrieve logs from containers. - Each procedure includes input validation for parameters such as `tail`, `since`, and `search`, ensuring robust access control and authorization checks. - Enhanced the `getContainerLogs` service to support fetching logs from both Docker containers and services, improving the logging capabilities of the application. This feature enhances observability and troubleshooting for users by providing direct access to container logs. --- .../dokploy/server/api/routers/application.ts | 38 ++++++++- apps/dokploy/server/api/routers/compose.ts | 42 +++++++++- apps/dokploy/server/api/routers/libsql.ts | 36 +++++++++ apps/dokploy/server/api/routers/mariadb.ts | 38 ++++++++- apps/dokploy/server/api/routers/mongo.ts | 36 +++++++++ apps/dokploy/server/api/routers/mysql.ts | 36 +++++++++ apps/dokploy/server/api/routers/postgres.ts | 36 +++++++++ apps/dokploy/server/api/routers/redis.ts | 36 +++++++++ packages/server/src/services/docker.ts | 81 +++++++++++++++++++ 9 files changed, 376 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index c00514715..228c98656 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -6,7 +6,9 @@ import { findEnvironmentById, findGitProviderById, findProjectById, + getAccessibleServerIds, getApplicationStats, + getContainerLogs, IS_CLOUD, mechanizeDockerContainer, readConfig, @@ -26,7 +28,6 @@ import { updateDeploymentStatus, writeConfig, writeConfigRemote, - getAccessibleServerIds, } from "@dokploy/server"; import { db } from "@dokploy/server/db"; import { @@ -1101,4 +1102,39 @@ export const applicationRouter = createTRPCRouter({ total: countResult[0]?.count ?? 0, }; }), + + readLogs: protectedProcedure + .input( + apiFindOneApplication.extend({ + tail: z.number().int().min(1).max(10000).default(100), + since: z + .string() + .regex(/^(all|\d+[smhd])$/, "Invalid since format") + .default("all"), + search: z + .string() + .regex(/^[a-zA-Z0-9 ._-]{0,500}$/) + .optional(), + }), + ) + .query(async ({ input, ctx }) => { + await checkServiceAccess(ctx, input.applicationId, "read"); + const application = await findApplicationById(input.applicationId); + if ( + application.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this application", + }); + } + return await getContainerLogs( + application.appName, + input.tail, + input.since, + input.search, + application.serverId, + ); + }), }); diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index be5a836d7..dd632466b 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -16,7 +16,9 @@ import { findGitProviderById, findProjectById, findServerById, + getAccessibleServerIds, getComposeContainer, + getContainerLogs, getWebServerSettings, IS_CLOUD, loadServices, @@ -30,7 +32,6 @@ import { stopCompose, updateCompose, updateDeploymentStatus, - getAccessibleServerIds, } from "@dokploy/server"; import { db } from "@dokploy/server/db"; import { @@ -1130,4 +1131,43 @@ export const composeRouter = createTRPCRouter({ total: countResult[0]?.count ?? 0, }; }), + + readLogs: protectedProcedure + .input( + apiFindCompose.extend({ + containerId: z + .string() + .min(1) + .regex(/^[a-zA-Z0-9.\-_]+$/, "Invalid container id."), + tail: z.number().int().min(1).max(10000).default(100), + since: z + .string() + .regex(/^(all|\d+[smhd])$/, "Invalid since format") + .default("all"), + search: z + .string() + .regex(/^[a-zA-Z0-9 ._-]{0,500}$/) + .optional(), + }), + ) + .query(async ({ input, ctx }) => { + await checkServiceAccess(ctx, input.composeId, "read"); + const compose = await findComposeById(input.composeId); + if ( + compose.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this compose", + }); + } + return await getContainerLogs( + input.containerId, + input.tail, + input.since, + input.search, + compose.serverId, + ); + }), }); diff --git a/apps/dokploy/server/api/routers/libsql.ts b/apps/dokploy/server/api/routers/libsql.ts index 65052097e..47798393e 100644 --- a/apps/dokploy/server/api/routers/libsql.ts +++ b/apps/dokploy/server/api/routers/libsql.ts @@ -6,6 +6,7 @@ import { findEnvironmentById, findLibsqlById, findProjectById, + getContainerLogs, IS_CLOUD, rebuildDatabase, removeLibsqlById, @@ -466,4 +467,39 @@ export const libsqlRouter = createTRPCRouter({ }); return true; }), + + readLogs: protectedProcedure + .input( + apiFindOneLibsql.extend({ + tail: z.number().int().min(1).max(10000).default(100), + since: z + .string() + .regex(/^(all|\d+[smhd])$/, "Invalid since format") + .default("all"), + search: z + .string() + .regex(/^[a-zA-Z0-9 ._-]{0,500}$/) + .optional(), + }), + ) + .query(async ({ input, ctx }) => { + await checkServiceAccess(ctx, input.libsqlId, "read"); + const libsql = await findLibsqlById(input.libsqlId); + if ( + libsql.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this LibSQL", + }); + } + return await getContainerLogs( + libsql.appName, + input.tail, + input.since, + input.search, + libsql.serverId, + ); + }), }); diff --git a/apps/dokploy/server/api/routers/mariadb.ts b/apps/dokploy/server/api/routers/mariadb.ts index a31554bec..a58a33a9c 100644 --- a/apps/dokploy/server/api/routers/mariadb.ts +++ b/apps/dokploy/server/api/routers/mariadb.ts @@ -9,6 +9,8 @@ import { findEnvironmentById, findMariadbById, findProjectById, + getAccessibleServerIds, + getContainerLogs, getServiceContainerCommand, IS_CLOUD, rebuildDatabase, @@ -19,7 +21,6 @@ import { stopService, stopServiceRemote, updateMariadbById, - getAccessibleServerIds, } from "@dokploy/server"; import { db } from "@dokploy/server/db"; import { @@ -590,4 +591,39 @@ export const mariadbRouter = createTRPCRouter({ ]); return { items, total: countResult[0]?.count ?? 0 }; }), + + readLogs: protectedProcedure + .input( + apiFindOneMariaDB.extend({ + tail: z.number().int().min(1).max(10000).default(100), + since: z + .string() + .regex(/^(all|\d+[smhd])$/, "Invalid since format") + .default("all"), + search: z + .string() + .regex(/^[a-zA-Z0-9 ._-]{0,500}$/) + .optional(), + }), + ) + .query(async ({ input, ctx }) => { + await checkServiceAccess(ctx, input.mariadbId, "read"); + const mariadb = await findMariadbById(input.mariadbId); + if ( + mariadb.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this MariaDB", + }); + } + return await getContainerLogs( + mariadb.appName, + input.tail, + input.since, + input.search, + mariadb.serverId, + ); + }), }); diff --git a/apps/dokploy/server/api/routers/mongo.ts b/apps/dokploy/server/api/routers/mongo.ts index f0f5c593a..222917b9f 100644 --- a/apps/dokploy/server/api/routers/mongo.ts +++ b/apps/dokploy/server/api/routers/mongo.ts @@ -10,6 +10,7 @@ import { findMongoById, findProjectById, getAccessibleServerIds, + getContainerLogs, getServiceContainerCommand, IS_CLOUD, rebuildDatabase, @@ -601,4 +602,39 @@ export const mongoRouter = createTRPCRouter({ ]); return { items, total: countResult[0]?.count ?? 0 }; }), + + readLogs: protectedProcedure + .input( + apiFindOneMongo.extend({ + tail: z.number().int().min(1).max(10000).default(100), + since: z + .string() + .regex(/^(all|\d+[smhd])$/, "Invalid since format") + .default("all"), + search: z + .string() + .regex(/^[a-zA-Z0-9 ._-]{0,500}$/) + .optional(), + }), + ) + .query(async ({ input, ctx }) => { + await checkServiceAccess(ctx, input.mongoId, "read"); + const mongo = await findMongoById(input.mongoId); + if ( + mongo.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this MongoDB", + }); + } + return await getContainerLogs( + mongo.appName, + input.tail, + input.since, + input.search, + mongo.serverId, + ); + }), }); diff --git a/apps/dokploy/server/api/routers/mysql.ts b/apps/dokploy/server/api/routers/mysql.ts index f2a750c04..263fa53f0 100644 --- a/apps/dokploy/server/api/routers/mysql.ts +++ b/apps/dokploy/server/api/routers/mysql.ts @@ -9,6 +9,7 @@ import { findEnvironmentById, findMySqlById, findProjectById, + getContainerLogs, getServiceContainerCommand, IS_CLOUD, rebuildDatabase, @@ -604,4 +605,39 @@ export const mysqlRouter = createTRPCRouter({ ]); return { items, total: countResult[0]?.count ?? 0 }; }), + + readLogs: protectedProcedure + .input( + apiFindOneMySql.extend({ + tail: z.number().int().min(1).max(10000).default(100), + since: z + .string() + .regex(/^(all|\d+[smhd])$/, "Invalid since format") + .default("all"), + search: z + .string() + .regex(/^[a-zA-Z0-9 ._-]{0,500}$/) + .optional(), + }), + ) + .query(async ({ input, ctx }) => { + await checkServiceAccess(ctx, input.mysqlId, "read"); + const mysql = await findMySqlById(input.mysqlId); + if ( + mysql.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this MySQL", + }); + } + return await getContainerLogs( + mysql.appName, + input.tail, + input.since, + input.search, + mysql.serverId, + ); + }), }); diff --git a/apps/dokploy/server/api/routers/postgres.ts b/apps/dokploy/server/api/routers/postgres.ts index 0baeb3f0e..33d8fd3f4 100644 --- a/apps/dokploy/server/api/routers/postgres.ts +++ b/apps/dokploy/server/api/routers/postgres.ts @@ -9,6 +9,7 @@ import { findEnvironmentById, findPostgresById, findProjectById, + getContainerLogs, getMountPath, getServiceContainerCommand, IS_CLOUD, @@ -614,4 +615,39 @@ export const postgresRouter = createTRPCRouter({ ]); return { items, total: countResult[0]?.count ?? 0 }; }), + + readLogs: protectedProcedure + .input( + apiFindOnePostgres.extend({ + tail: z.number().int().min(1).max(10000).default(100), + since: z + .string() + .regex(/^(all|\d+[smhd])$/, "Invalid since format") + .default("all"), + search: z + .string() + .regex(/^[a-zA-Z0-9 ._-]{0,500}$/) + .optional(), + }), + ) + .query(async ({ input, ctx }) => { + await checkServiceAccess(ctx, input.postgresId, "read"); + const postgres = await findPostgresById(input.postgresId); + if ( + postgres.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this Postgres", + }); + } + return await getContainerLogs( + postgres.appName, + input.tail, + input.since, + input.search, + postgres.serverId, + ); + }), }); diff --git a/apps/dokploy/server/api/routers/redis.ts b/apps/dokploy/server/api/routers/redis.ts index 2d7b20674..a1e912e0b 100644 --- a/apps/dokploy/server/api/routers/redis.ts +++ b/apps/dokploy/server/api/routers/redis.ts @@ -8,6 +8,7 @@ import { findEnvironmentById, findProjectById, findRedisById, + getContainerLogs, getServiceContainerCommand, IS_CLOUD, rebuildDatabase, @@ -587,4 +588,39 @@ export const redisRouter = createTRPCRouter({ ]); return { items, total: countResult[0]?.count ?? 0 }; }), + + readLogs: protectedProcedure + .input( + apiFindOneRedis.extend({ + tail: z.number().int().min(1).max(10000).default(100), + since: z + .string() + .regex(/^(all|\d+[smhd])$/, "Invalid since format") + .default("all"), + search: z + .string() + .regex(/^[a-zA-Z0-9 ._-]{0,500}$/) + .optional(), + }), + ) + .query(async ({ input, ctx }) => { + await checkServiceAccess(ctx, input.redisId, "read"); + const redis = await findRedisById(input.redisId); + if ( + redis.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this Redis", + }); + } + return await getContainerLogs( + redis.appName, + input.tail, + input.since, + input.search, + redis.serverId, + ); + }), }); diff --git a/packages/server/src/services/docker.ts b/packages/server/src/services/docker.ts index 1205e62c2..2b14d0b23 100644 --- a/packages/server/src/services/docker.ts +++ b/packages/server/src/services/docker.ts @@ -354,6 +354,87 @@ export const getContainersByAppLabel = async ( return []; }; +export const getContainerLogs = async ( + appName: string, + tail = 100, + since = "all", + search?: string, + serverId?: string | null, +): Promise => { + // First, find the real container ID by appName filter + const findCommand = `docker ps -q --filter "name=^${appName}" | head -1`; + const findResult = serverId + ? await execAsyncRemote(serverId, findCommand) + : await execAsync(findCommand); + + const containerId = findResult.stdout.trim(); + if (!containerId) { + // Fallback: try as a swarm service + const svcCommand = `docker service ls -q --filter "name=${appName}" | head -1`; + const svcResult = serverId + ? await execAsyncRemote(serverId, svcCommand) + : await execAsync(svcCommand); + + const serviceId = svcResult.stdout.trim(); + if (!serviceId) { + throw new Error(`No container or service found for: ${appName}`); + } + + // Use docker service logs for swarm + const sinceFlag = since === "all" ? "" : `--since ${since}`; + const baseCommand = `docker service logs --timestamps --raw --tail ${tail} ${sinceFlag} ${appName}`; + const escapedSearch = search?.replace(/'/g, "'\\''") ?? ""; + const command = search + ? `${baseCommand} 2>&1 | grep -iF '${escapedSearch}'` + : `${baseCommand} 2>&1`; + + try { + const result = serverId + ? await execAsyncRemote(serverId, command) + : await execAsync(command); + return result.stdout; + } catch (error: unknown) { + if ( + error && + typeof error === "object" && + "stdout" in error && + typeof (error as { stdout: string }).stdout === "string" && + (error as { stdout: string }).stdout.length > 0 + ) { + return (error as { stdout: string }).stdout; + } + throw error; + } + } + + const sinceFlag = since === "all" ? "" : `--since ${since}`; + const baseCommand = `docker container logs --timestamps --tail ${tail} ${sinceFlag} ${containerId}`; + + const escapedSearch = search?.replace(/'/g, "'\\''") ?? ""; + const command = search + ? `${baseCommand} 2>&1 | grep -iF '${escapedSearch}'` + : `${baseCommand} 2>&1`; + + try { + const result = serverId + ? await execAsyncRemote(serverId, command) + : await execAsync(command); + + return result.stdout; + } catch (error: unknown) { + if ( + error && + typeof error === "object" && + "stdout" in error && + typeof (error as { stdout: string }).stdout === "string" && + (error as { stdout: string }).stdout.length > 0 + ) { + return (error as { stdout: string }).stdout; + } + throw error; + } +}; + export const containerRestart = async (containerId: string) => { try { const { stdout, stderr } = await execAsync( From b8db120432c966810f88ee66cf638f0be7fd0c9d Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Thu, 9 Apr 2026 11:41:01 -0600 Subject: [PATCH 5/6] refactor: enhance getContainerLogs function to support app name or ID - Updated the `getContainerLogs` function to accept either an application name or container ID, improving flexibility in log retrieval. - Simplified the command execution logic by consolidating the remote and local execution paths. - Added a new parameter to directly use container IDs, streamlining the process for users. These changes enhance the usability of the logging feature, allowing for more efficient access to container logs. --- apps/dokploy/server/api/routers/compose.ts | 1 + packages/server/src/services/docker.ts | 72 ++++++++-------------- 2 files changed, 28 insertions(+), 45 deletions(-) diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index dd632466b..d395bdffc 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -1168,6 +1168,7 @@ export const composeRouter = createTRPCRouter({ input.since, input.search, compose.serverId, + true, ); }), }); diff --git a/packages/server/src/services/docker.ts b/packages/server/src/services/docker.ts index 2b14d0b23..eb218b1e1 100644 --- a/packages/server/src/services/docker.ts +++ b/packages/server/src/services/docker.ts @@ -355,60 +355,45 @@ export const getContainersByAppLabel = async ( }; export const getContainerLogs = async ( - appName: string, + appNameOrId: string, tail = 100, since = "all", search?: string, serverId?: string | null, + useContainerIdDirectly = false, ): Promise => { - // First, find the real container ID by appName filter - const findCommand = `docker ps -q --filter "name=^${appName}" | head -1`; - const findResult = serverId - ? await execAsyncRemote(serverId, findCommand) - : await execAsync(findCommand); + const exec = (cmd: string) => + serverId ? execAsyncRemote(serverId, cmd) : execAsync(cmd); - const containerId = findResult.stdout.trim(); - if (!containerId) { - // Fallback: try as a swarm service - const svcCommand = `docker service ls -q --filter "name=${appName}" | head -1`; - const svcResult = serverId - ? await execAsyncRemote(serverId, svcCommand) - : await execAsync(svcCommand); + let target = appNameOrId; + let isService = false; - const serviceId = svcResult.stdout.trim(); - if (!serviceId) { - throw new Error(`No container or service found for: ${appName}`); - } + if (!useContainerIdDirectly) { + // Find the real container ID by appName filter + const findResult = await exec( + `docker ps -q --filter "name=^${appNameOrId}" | head -1`, + ); + const containerId = findResult.stdout.trim(); - // Use docker service logs for swarm - const sinceFlag = since === "all" ? "" : `--since ${since}`; - const baseCommand = `docker service logs --timestamps --raw --tail ${tail} ${sinceFlag} ${appName}`; - const escapedSearch = search?.replace(/'/g, "'\\''") ?? ""; - const command = search - ? `${baseCommand} 2>&1 | grep -iF '${escapedSearch}'` - : `${baseCommand} 2>&1`; - - try { - const result = serverId - ? await execAsyncRemote(serverId, command) - : await execAsync(command); - return result.stdout; - } catch (error: unknown) { - if ( - error && - typeof error === "object" && - "stdout" in error && - typeof (error as { stdout: string }).stdout === "string" && - (error as { stdout: string }).stdout.length > 0 - ) { - return (error as { stdout: string }).stdout; + if (!containerId) { + // Fallback: try as a swarm service + const svcResult = await exec( + `docker service ls -q --filter "name=${appNameOrId}" | head -1`, + ); + const serviceId = svcResult.stdout.trim(); + if (!serviceId) { + throw new Error(`No container or service found for: ${appNameOrId}`); } - throw error; + isService = true; + } else { + target = containerId; } } const sinceFlag = since === "all" ? "" : `--since ${since}`; - const baseCommand = `docker container logs --timestamps --tail ${tail} ${sinceFlag} ${containerId}`; + const baseCommand = isService + ? `docker service logs --timestamps --raw --tail ${tail} ${sinceFlag} ${target}` + : `docker container logs --timestamps --tail ${tail} ${sinceFlag} ${target}`; const escapedSearch = search?.replace(/'/g, "'\\''") ?? ""; const command = search @@ -416,10 +401,7 @@ export const getContainerLogs = async ( : `${baseCommand} 2>&1`; try { - const result = serverId - ? await execAsyncRemote(serverId, command) - : await execAsync(command); - + const result = await exec(command); return result.stdout; } catch (error: unknown) { if ( From 6c3578a475f0479d2ed80b5253cd31c23308bb60 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Thu, 9 Apr 2026 11:44:55 -0600 Subject: [PATCH 6/6] feat: enhance AnalyzeLogs component with AI provider configuration prompt - Updated the AnalyzeLogs component to display a message and button for configuring AI providers when none are available, improving user guidance. - Added a link to the settings page for easy access to AI provider configuration. - Integrated new icon for the configuration button to enhance UI clarity. These changes improve the user experience by ensuring users are informed about the need to set up AI providers for log analysis. --- .../dashboard/docker/logs/analyze-logs.tsx | 90 +++++++++++-------- 1 file changed, 53 insertions(+), 37 deletions(-) diff --git a/apps/dokploy/components/dashboard/docker/logs/analyze-logs.tsx b/apps/dokploy/components/dashboard/docker/logs/analyze-logs.tsx index c26ce5bfe..267735eac 100644 --- a/apps/dokploy/components/dashboard/docker/logs/analyze-logs.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/analyze-logs.tsx @@ -1,5 +1,6 @@ "use client"; -import { Bot, Loader2, RotateCcw, X } from "lucide-react"; +import { Bot, Loader2, RotateCcw, Settings, X } from "lucide-react"; +import Link from "next/link"; import { useState } from "react"; import ReactMarkdown from "react-markdown"; import { toast } from "sonner"; @@ -91,42 +92,57 @@ export function AnalyzeLogs({ logs, context }: Props) {
{!data?.analysis ? ( - <> - - - + providers && providers.length === 0 ? ( +
+

+ No AI providers configured. Set up a provider to start + analyzing logs. +

+ +
+ ) : ( + <> + + + + ) ) : ( <>