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 */} diff --git a/src/app/(dashboard)/pipelines/[id]/page.tsx b/src/app/(dashboard)/pipelines/[id]/page.tsx index 89e63c2a..502af3a4 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( @@ -202,14 +200,27 @@ 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. + // Use dataUpdatedAt (stable timestamp from React Query) instead of Date.now() + // to satisfy react-hooks/purity (no impure calls) and avoid useEffect+setState. + 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(pipelineQuery.dataUpdatedAt - minUptime * 1000); + }, [pipelineQuery.data?.nodeStatuses, pipelineQuery.dataUpdatedAt]); + + // 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; @@ -464,7 +475,6 @@ function PipelineBuilderInner({ pipelineId }: { pipelineId: string }) { onDiscardChanges={() => setDiscardOpen(true)} aiEnabled={aiEnabled} onAiOpen={() => setAiDialogOpen(true)} - onDebugOpen={() => setDebugPanelOpen(true)} deployedVersionNumber={pipelineQuery.data?.deployedVersionNumber} />
@@ -582,13 +592,6 @@ function PipelineBuilderInner({ pipelineId }: { pipelineId: string }) { onOpenChange={setAiDialogOpen} pipelineId={pipelineId} environmentName={pipelineQuery.data?.environment?.name} - /> - )} - {aiEnabled && ( - )} diff --git a/src/app/(dashboard)/pipelines/page.tsx b/src/app/(dashboard)/pipelines/page.tsx index 2f8b3131..1c6abafc 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 == null) 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 ? ( 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 */} -
-
-