From 844655d1c88840fa68cd534c876eb2b3b4634b27 Mon Sep 17 00:00:00 2001 From: menzelj Date: Tue, 16 Jun 2026 07:58:26 +0000 Subject: [PATCH] 0.34.1: populate the log container filter (parse compose prefix) The whole-stack log stream sends service:null on every line, so the container filter dropdown only ever showed "All containers". docker compose logs already prefixes each line with the container name (and an RFC3339 timestamp via --timestamps); parse that prefix client-side to recover the container, populate the filter, and render time + container + message separately (cleaner than the raw prefixed line). Co-Authored-By: Claude Opus 4.8 --- frontend/package.json | 2 +- frontend/src/components/stacks/LogViewer.tsx | 51 ++++++++++++++++++-- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 7c36534..b03c0dc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "stackpilot-frontend", "private": true, - "version": "0.34.0", + "version": "0.34.1", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/components/stacks/LogViewer.tsx b/frontend/src/components/stacks/LogViewer.tsx index dff2b7e..7439661 100644 --- a/frontend/src/components/stacks/LogViewer.tsx +++ b/frontend/src/components/stacks/LogViewer.tsx @@ -48,7 +48,44 @@ const levelStyles: Record = { debug: { text: "text-slate-400", row: "border-l-2 border-transparent" }, }; -type LogLine = { service: string | null; line: string; level: Level }; +// `docker compose logs` prefixes every line with the container name (padded +// for alignment) then `| `; with --timestamps an RFC3339 stamp follows. The +// whole-stack stream sends service:null, so we recover the container here. +const prefixRe = /^(\S+)\s+\|\s?(.*)$/s; +const tsRe = /^(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)\s(.*)$/s; + +function parseLine( + rawService: string | null, + raw: string +): { service: string | null; time: string | null; msg: string } { + let service = rawService; + let body = raw; + if (!service) { + const m = prefixRe.exec(raw); + if (m) { + service = m[1]; + body = m[2]; + } + } + let time: string | null = null; + const t = tsRe.exec(body); + if (t) { + const d = new Date(t[1]); + if (!Number.isNaN(d.getTime())) { + time = d.toLocaleTimeString(); + body = t[2]; + } + } + return { service, time, msg: body }; +} + +type LogLine = { + service: string | null; + time: string | null; + msg: string; + raw: string; + level: Level; +}; export function LogViewer({ stackId, agentId }: { stackId: string; agentId?: number }) { const [lines, setLines] = useState([]); @@ -83,10 +120,11 @@ export function LogViewer({ stackId, agentId }: { stackId: string; agentId?: num try { const msg = JSON.parse(ev.data); if (msg.type === "log") { + const { service, time, msg: body } = parseLine(msg.service, msg.line); setLines((prev) => { const next = [ ...prev, - { service: msg.service, line: msg.line, level: levelFor(msg.line) }, + { service, time, msg: body, raw: msg.line, level: levelFor(body) }, ]; return next.length > MAX_LINES ? next.slice(-MAX_LINES) : next; }); @@ -112,7 +150,7 @@ export function LogViewer({ stackId, agentId }: { stackId: string; agentId?: num const q = search.trim().toLowerCase(); return lines.filter((l) => { if (serviceFilter !== "__all__" && l.service !== serviceFilter) return false; - if (q && !l.line.toLowerCase().includes(q)) return false; + if (q && !l.msg.toLowerCase().includes(q)) return false; return true; }); }, [lines, serviceFilter, search]); @@ -124,7 +162,7 @@ export function LogViewer({ stackId, agentId }: { stackId: string; agentId?: num }, [filtered, autoScroll]); const download = () => { - const blob = new Blob([filtered.map((l) => l.line).join("\n")], { + const blob = new Blob([filtered.map((l) => l.raw).join("\n")], { type: "text/plain", }); const a = document.createElement("a"); @@ -212,10 +250,13 @@ export function LogViewer({ stackId, agentId }: { stackId: string; agentId?: num key={i} className={`whitespace-pre-wrap break-all py-0.5 pl-2 ${style.row}`} > + {l.time && ( + {l.time} + )} {l.service && ( {l.service} )} - {l.line} + {l.msg} ); })}