diff --git a/apps/dokploy/components/dashboard/docker/logs/utils.ts b/apps/dokploy/components/dashboard/docker/logs/utils.ts index fc0b50928..b8cc03052 100644 --- a/apps/dokploy/components/dashboard/docker/logs/utils.ts +++ b/apps/dokploy/components/dashboard/docker/logs/utils.ts @@ -41,6 +41,43 @@ const LOG_STYLES: Record = { }, } as const; +// Collapse Docker progress lines that update the same image (same hash prefix) +function collapseProgressLines(lines: string[]): string[] { + const result: string[] = []; + + for (const line of lines) { + // Detect Docker progress lines: start with 12-char hash followed by space and [ + // Pattern: [progress bar] or just + const progressMatch = line.match(/^([a-f0-9]{12})(\s+\[.*\].*)?$/i); + + if (progressMatch) { + const hash = progressMatch[1]; + // Search backwards for the last line with the same hash + let found = false; + for (let i = result.length - 1; i >= 0; i--) { + const existingMatch = result[i]?.match( + /^([a-f0-9]{12})(\s+\[.*\].*)?$/i, + ); + if (existingMatch && existingMatch[1] === hash) { + // Replace the existing line with the new progress update + result[i] = line; + found = true; + break; + } + } + if (!found) { + // First occurrence of this hash, add the line + result.push(line); + } + } else { + // Not a progress line, add normally + result.push(line); + } + } + + return result; +} + export function parseLogs(logString: string): LogLine[] { // Regex to match the log line format // Example of return : @@ -51,13 +88,32 @@ export function parseLogs(logString: string): LogLine[] { const logRegex = /^(?:(?\d+)\s+)?(?(?:\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z|\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} UTC))?\s*(?[\s\S]*)$/; - return logString - .split("\n") + // Normalize \r\n to \n, then handle \r (progress updates) by splitting and processing + const normalized = logString.replace(/\r\n/g, "\n"); + const lines = normalized + .split(/\r/) + .flatMap((segment) => segment.split("\n")); + + const trimmedLines = lines .map((line) => line.trim()) - .filter((line) => line !== "") + .filter((line) => line !== ""); + const collapsedLines = collapseProgressLines(trimmedLines); + + return collapsedLines .map((line) => { const match = line.match(logRegex); - if (!match) return null; + + if (!match) { + // If line doesn't match the standard format but has content, treat it as a message without timestamp + // This handles progress lines and other output without standard format + return line.trim() + ? { + rawTimestamp: null, + timestamp: null, + message: line.trim(), + } + : null; + } const { timestamp, message } = match.groups ?? {};