From d77c366b9650ee3340bfbae035a24f7f9dbbe48b Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 26 Mar 2026 19:35:22 +0000 Subject: [PATCH 1/9] fix: disable refetchOnWindowFocus to prevent modals closing on tab switch --- src/trpc/client.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/trpc/client.tsx b/src/trpc/client.tsx index 91df07bb..e5fc1979 100644 --- a/src/trpc/client.tsx +++ b/src/trpc/client.tsx @@ -23,7 +23,7 @@ export function TRPCClientProvider({ () => new QueryClient({ defaultOptions: { - queries: { staleTime: 5 * 1000 }, + queries: { staleTime: 5 * 1000, refetchOnWindowFocus: false }, }, }) ); From b9e455ec349f1bab76c1ebd8176672d1d8e6652c Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 26 Mar 2026 19:35:22 +0000 Subject: [PATCH 2/9] fix: swap fleet tab order so Nodes (default) is on the left --- src/app/(dashboard)/fleet/page.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/(dashboard)/fleet/page.tsx b/src/app/(dashboard)/fleet/page.tsx index 148a7605..08ceb331 100644 --- a/src/app/(dashboard)/fleet/page.tsx +++ b/src/app/(dashboard)/fleet/page.tsx @@ -186,15 +186,15 @@ export default function FleetPage() { return (
+ + Nodes + Overview - - Nodes -
{/* Toolbar — shown when not loading and nodes exist or filters active */} From 9272798134fc20d404949f28a02e66d7d4610fdf Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 26 Mar 2026 19:36:39 +0000 Subject: [PATCH 3/9] feat: add minUptimeSeconds to pipeline list query --- src/server/services/pipeline-graph.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/server/services/pipeline-graph.ts b/src/server/services/pipeline-graph.ts index 3a283604..a47cec30 100644 --- a/src/server/services/pipeline-graph.ts +++ b/src/server/services/pipeline-graph.ts @@ -540,6 +540,7 @@ export async function listPipelinesForEnvironment(environmentId: string) { eventsDiscarded: true, bytesIn: true, bytesOut: true, + uptimeSeconds: true, }, }, nodes: { @@ -625,6 +626,12 @@ export async function listPipelinesForEnvironment(environmentId: string) { .map((n) => n.sharedComponent!.name), upstreamDepCount: p._count.upstreamDeps, downstreamDepCount: p._count.downstreamDeps, + minUptimeSeconds: (() => { + const runningUptimes = p.nodeStatuses + .filter((s) => s.status === "RUNNING" && s.uptimeSeconds != null) + .map((s) => s.uptimeSeconds!); + return runningUptimes.length > 0 ? Math.min(...runningUptimes) : null; + })(), }; })); } From 7784c3023232b7a5d5c43e464641002f01b8cb1c Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 26 Mar 2026 19:37:01 +0000 Subject: [PATCH 4/9] feat: add uptime column to pipeline list table --- src/app/(dashboard)/pipelines/page.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/app/(dashboard)/pipelines/page.tsx b/src/app/(dashboard)/pipelines/page.tsx index 2f8b3131..470e42f2 100644 --- a/src/app/(dashboard)/pipelines/page.tsx +++ b/src/app/(dashboard)/pipelines/page.tsx @@ -124,6 +124,15 @@ function getReductionPercent(totals: { return Math.max(0, (1 - evOut / evIn) * 100); } +function formatUptime(seconds: number | null): string { + if (!seconds) return "\u2014"; + if (seconds < 60) return `${seconds}s`; + if (seconds < 3600) return `${Math.floor(seconds / 60)}m`; + if (seconds < 86400) + return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`; + return `${Math.floor(seconds / 86400)}d ${Math.floor((seconds % 86400) / 3600)}h`; +} + /** Derive display status string for a pipeline row. */ function derivePipelineStatus( pipeline: { isDraft: boolean; nodeStatuses: Array<{ status: string }> }, @@ -587,6 +596,7 @@ export default function PipelinesPage() { currentDirection={sortDirection} onSort={handleSort} /> + Uptime Health + {/* Uptime */} + + {formatUptime(pipeline.minUptimeSeconds)} + {/* Health — batch data instead of per-row query */} {pipeline.isDraft ? ( From 66b124a701abff629284d8ee626c2672377aa5c7 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 26 Mar 2026 19:37:23 +0000 Subject: [PATCH 5/9] fix: scope error badge to current pipeline session using uptimeSeconds --- src/app/(dashboard)/pipelines/[id]/page.tsx | 23 +++++++++++++++------ src/server/routers/pipeline.ts | 2 +- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/app/(dashboard)/pipelines/[id]/page.tsx b/src/app/(dashboard)/pipelines/[id]/page.tsx index 89e63c2a..d1ae0b78 100644 --- a/src/app/(dashboard)/pipelines/[id]/page.tsx +++ b/src/app/(dashboard)/pipelines/[id]/page.tsx @@ -202,14 +202,25 @@ function PipelineBuilderInner({ pipelineId }: { pipelineId: string }) { ), ); - // Lightweight check for recent errors (for toolbar badge) — 24h window - const [errorCheckSince] = useState( - () => new Date(Date.now() - 24 * 60 * 60 * 1000), - ); + // Compute session start from minimum uptime across all running nodes + const sessionStart = useMemo(() => { + const statuses = pipelineQuery.data?.nodeStatuses; + if (!statuses || statuses.length === 0) return null; + const uptimes = statuses + .filter((s: { status: string; uptimeSeconds: number | null }) => + s.status === "RUNNING" && s.uptimeSeconds != null + ) + .map((s: { uptimeSeconds: number | null }) => s.uptimeSeconds!); + if (uptimes.length === 0) return null; + const minUptime = Math.min(...uptimes); + return new Date(Date.now() - minUptime * 1000); + }, [pipelineQuery.data?.nodeStatuses]); + + // Lightweight check for recent errors (for toolbar badge) — scoped to current session const recentErrorsQuery = useQuery( trpc.pipeline.logs.queryOptions( - { pipelineId, levels: ["ERROR"], limit: 1, since: errorCheckSince }, - { enabled: !!isDeployed && !logsOpen, refetchInterval: 10000 }, + { pipelineId, levels: ["ERROR"], limit: 1, since: sessionStart! }, + { enabled: !!isDeployed && !logsOpen && !!sessionStart, refetchInterval: 10000 }, ), ); const hasRecentErrors = (recentErrorsQuery.data?.items?.length ?? 0) > 0; diff --git a/src/server/routers/pipeline.ts b/src/server/routers/pipeline.ts index 2b05a640..27a28e96 100644 --- a/src/server/routers/pipeline.ts +++ b/src/server/routers/pipeline.ts @@ -90,7 +90,7 @@ export const pipelineRouter = router({ edges: true, environment: { select: { teamId: true, gitOpsMode: true, name: true } }, nodeStatuses: { - select: { status: true }, + select: { status: true, uptimeSeconds: true }, }, }, }); From 6dc6947cfdd830445aca6f16e3211d56f3eaf6e0 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 26 Mar 2026 19:39:39 +0000 Subject: [PATCH 6/9] refactor: remove debug button and panel in prep for AI dialog merge --- src/app/(dashboard)/pipelines/[id]/page.tsx | 10 ----- src/components/flow/flow-toolbar.tsx | 47 ++++++--------------- 2 files changed, 14 insertions(+), 43 deletions(-) diff --git a/src/app/(dashboard)/pipelines/[id]/page.tsx b/src/app/(dashboard)/pipelines/[id]/page.tsx index d1ae0b78..a4f4e56e 100644 --- a/src/app/(dashboard)/pipelines/[id]/page.tsx +++ b/src/app/(dashboard)/pipelines/[id]/page.tsx @@ -33,7 +33,6 @@ import { ComponentPalette } from "@/components/flow/component-palette"; import { FlowCanvas } from "@/components/flow/flow-canvas"; import { FlowToolbar } from "@/components/flow/flow-toolbar"; import { AiPipelineDialog } from "@/components/flow/ai-pipeline-dialog"; -import { AiDebugPanel } from "@/components/flow/ai-debug-panel"; import { DetailPanel } from "@/components/flow/detail-panel"; import { DeployDialog } from "@/components/flow/deploy-dialog"; import { SaveTemplateDialog } from "@/components/flow/save-template-dialog"; @@ -128,7 +127,6 @@ function PipelineBuilderInner({ pipelineId }: { pipelineId: string }) { const [metricsOpen, setMetricsOpen] = useState(false); const [logsOpen, setLogsOpen] = useState(false); const [aiDialogOpen, setAiDialogOpen] = useState(false); - const [debugPanelOpen, setDebugPanelOpen] = useState(false); const selectedTeamId = useTeamStore((s) => s.selectedTeamId); const teamQuery = useQuery( @@ -475,7 +473,6 @@ function PipelineBuilderInner({ pipelineId }: { pipelineId: string }) { onDiscardChanges={() => setDiscardOpen(true)} aiEnabled={aiEnabled} onAiOpen={() => setAiDialogOpen(true)} - onDebugOpen={() => setDebugPanelOpen(true)} deployedVersionNumber={pipelineQuery.data?.deployedVersionNumber} />
@@ -593,13 +590,6 @@ function PipelineBuilderInner({ pipelineId }: { pipelineId: string }) { onOpenChange={setAiDialogOpen} pipelineId={pipelineId} environmentName={pipelineQuery.data?.environment?.name} - /> - )} - {aiEnabled && ( - )} diff --git a/src/components/flow/flow-toolbar.tsx b/src/components/flow/flow-toolbar.tsx index aecbdc01..b4a011f1 100644 --- a/src/components/flow/flow-toolbar.tsx +++ b/src/components/flow/flow-toolbar.tsx @@ -22,7 +22,6 @@ import { Clock, X, Sparkles, - Bug, } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; @@ -76,7 +75,6 @@ interface FlowToolbarProps { onDiscardChanges?: () => void; aiEnabled?: boolean; onAiOpen?: () => void; - onDebugOpen?: () => void; deployedVersionNumber?: number | null; } @@ -111,7 +109,6 @@ export function FlowToolbar({ onDiscardChanges, aiEnabled, onAiOpen, - onDebugOpen, deployedVersionNumber, }: FlowToolbarProps) { const globalConfig = useFlowStore((s) => s.globalConfig); @@ -341,36 +338,20 @@ export function FlowToolbar({ {aiEnabled && ( - <> - - - - - AI pipeline builder - - - - - - Debug with AI - - + + + + + AI assistant + )} {pipelineId && ( From d149bbfda0b2dabbf11342c257173b977bf9fb50 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 26 Mar 2026 19:41:25 +0000 Subject: [PATCH 7/9] feat: merge AI builder and debug into unified three-tab dialog --- src/components/flow/ai-debug-panel.tsx | 229 --------------------- src/components/flow/ai-pipeline-dialog.tsx | 191 ++++++++++++++++- 2 files changed, 185 insertions(+), 235 deletions(-) delete mode 100644 src/components/flow/ai-debug-panel.tsx diff --git a/src/components/flow/ai-debug-panel.tsx b/src/components/flow/ai-debug-panel.tsx deleted file mode 100644 index 8dbc9541..00000000 --- a/src/components/flow/ai-debug-panel.tsx +++ /dev/null @@ -1,229 +0,0 @@ -"use client"; - -import { useState, useRef, useEffect, useCallback } from "react"; -import { - Bug, - Bot, - User, - Loader2, - Send, - AlertTriangle, - MessageSquarePlus, -} from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { useAiDebugConversation } from "@/hooks/use-ai-debug-conversation"; - -interface AiDebugPanelProps { - open: boolean; - onOpenChange: (open: boolean) => void; - pipelineId: string; - currentYaml?: string; -} - -export function AiDebugPanel({ - open, - onOpenChange, - pipelineId, - currentYaml, -}: AiDebugPanelProps) { - const [prompt, setPrompt] = useState(""); - const textareaRef = useRef(null); - const messagesEndRef = useRef(null); - - const conversation = useAiDebugConversation({ pipelineId, currentYaml }); - - // Auto-scroll to bottom on new messages or streaming content - useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [conversation.messages, conversation.streamingContent]); - - // Auto-grow textarea - useEffect(() => { - const ta = textareaRef.current; - if (!ta) return; - ta.style.height = "auto"; - const maxHeight = 4 * 24; // 4 lines - ta.style.height = `${Math.min(ta.scrollHeight, maxHeight)}px`; - }, [prompt]); - - const handleSubmit = useCallback( - (e?: React.FormEvent) => { - e?.preventDefault(); - if (!prompt.trim()) return; - const message = prompt; - setPrompt(""); - conversation.sendMessage(message); - }, - [prompt, conversation], - ); - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - handleSubmit(); - } - }; - - return ( - - - - - - Debug with AI - - - Ask questions about your pipeline's configuration, metrics, and - errors - - - - {conversation.isLoading ? ( -
- -
- ) : ( -
- {/* Message thread */} -
-
- {conversation.messages.length === 0 && - !conversation.isStreaming && ( -

- Ask the AI to help debug your pipeline — it has access to - your configuration, metrics, SLI health, and recent error - logs. -

- )} - - {conversation.messages.map((msg) => ( -
-
- {msg.role === "user" ? ( - - ) : ( - - )} -
-
-
-
- {msg.content} -
-
-
-
- ))} - - {/* Streaming content */} - {conversation.isStreaming && conversation.streamingContent && ( -
-
- -
-
-
- {conversation.streamingContent} -
-
-
- )} - - {/* Streaming placeholder (waiting for first token) */} - {conversation.isStreaming && !conversation.streamingContent && ( -
- - Analyzing pipeline... -
- )} - -
-
-
- - {/* Error display */} - {conversation.error && ( -
- - {conversation.error} -
- )} - - {/* Input pinned at bottom */} -
-
-