diff --git a/src/app/(dashboard)/pipelines/[id]/metrics/page.tsx b/src/app/(dashboard)/pipelines/[id]/metrics/page.tsx
index 7118b01..45517b4 100644
--- a/src/app/(dashboard)/pipelines/[id]/metrics/page.tsx
+++ b/src/app/(dashboard)/pipelines/[id]/metrics/page.tsx
@@ -9,6 +9,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { SummaryCards } from "@/components/metrics/summary-cards";
import { MetricsChart } from "@/components/metrics/component-chart";
+import { PipelineLogs } from "@/components/pipeline/pipeline-logs";
const TIME_RANGES = [
{ label: "5m", minutes: 5 },
@@ -116,6 +117,18 @@ export default function PipelineMetricsPage() {
>
)}
+
+ {/* Pipeline Logs */}
+
+
+ Logs
+
+
+
+
+
);
}
diff --git a/src/app/(dashboard)/pipelines/[id]/page.tsx b/src/app/(dashboard)/pipelines/[id]/page.tsx
index db87144..49bb9bd 100644
--- a/src/app/(dashboard)/pipelines/[id]/page.tsx
+++ b/src/app/(dashboard)/pipelines/[id]/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useCallback, useEffect, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import type { NodeMetricsData } from "@/stores/flow-store";
@@ -34,6 +34,7 @@ import { DeployDialog } from "@/components/flow/deploy-dialog";
import { SaveTemplateDialog } from "@/components/flow/save-template-dialog";
import { ConfirmDialog } from "@/components/confirm-dialog";
import { PipelineMetricsChart } from "@/components/pipeline/metrics-chart";
+import { PipelineLogs } from "@/components/pipeline/pipeline-logs";
function aggregateProcessStatus(
statuses: Array<{ status: string }>
@@ -114,6 +115,7 @@ function PipelineBuilderInner({ pipelineId }: { pipelineId: string }) {
const [deleteOpen, setDeleteOpen] = useState(false);
const [undeployOpen, setUndeployOpen] = useState(false);
const [metricsOpen, setMetricsOpen] = useState(false);
+ const [logsOpen, setLogsOpen] = useState(false);
const loadGraph = useFlowStore((s) => s.loadGraph);
const isDirty = useFlowStore((s) => s.isDirty);
@@ -157,6 +159,20 @@ function PipelineBuilderInner({ pipelineId }: { pipelineId: string }) {
),
);
+ // Lightweight check for recent errors (for toolbar badge) — 24h window
+ const errorCheckSince = useMemo(
+ () => new Date(Date.now() - 24 * 60 * 60 * 1000),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [],
+ );
+ const recentErrorsQuery = useQuery(
+ trpc.pipeline.logs.queryOptions(
+ { pipelineId, levels: ["ERROR"], limit: 1, since: errorCheckSince },
+ { enabled: !!isDeployed && !logsOpen, refetchInterval: 10000 },
+ ),
+ );
+ const hasRecentErrors = (recentErrorsQuery.data?.items?.length ?? 0) > 0;
+
// Merge component metrics into flow node data
useEffect(() => {
const components = componentMetricsQuery.data?.components;
@@ -367,6 +383,9 @@ function PipelineBuilderInner({ pipelineId }: { pipelineId: string }) {
isDirty={isDirty}
metricsOpen={metricsOpen}
onToggleMetrics={() => setMetricsOpen((v) => !v)}
+ logsOpen={logsOpen}
+ onToggleLogs={() => setLogsOpen((v) => !v)}
+ hasRecentErrors={hasRecentErrors}
processStatus={
pipelineQuery.data?.nodeStatuses
? aggregateProcessStatus(pipelineQuery.data.nodeStatuses)
@@ -417,6 +436,11 @@ function PipelineBuilderInner({ pipelineId }: { pipelineId: string }) {
)}
+ {logsOpen && (
+
+ )}
void;
+ logsOpen?: boolean;
+ onToggleLogs?: () => void;
+ hasRecentErrors?: boolean;
processStatus?: ProcessStatusValue | null;
}
@@ -87,6 +91,9 @@ export function FlowToolbar({
isDirty = false,
metricsOpen = false,
onToggleMetrics,
+ logsOpen = false,
+ onToggleLogs,
+ hasRecentErrors = false,
processStatus,
}: FlowToolbarProps) {
const globalConfig = useFlowStore((s) => s.globalConfig);
@@ -317,6 +324,26 @@ export function FlowToolbar({
)}
+ {onToggleLogs && (
+
+
+
+
+ {logsOpen ? "Hide logs" : "Show logs"}
+
+ )}
+
diff --git a/src/components/pipeline/pipeline-logs.tsx b/src/components/pipeline/pipeline-logs.tsx
index 53f5b9c..9d7e9b8 100644
--- a/src/components/pipeline/pipeline-logs.tsx
+++ b/src/components/pipeline/pipeline-logs.tsx
@@ -166,8 +166,12 @@ export function PipelineLogs({ pipelineId, nodeId }: PipelineLogsProps) {
{log.level}
{" "}
- [{log.node.name}]
- {" "}
+ {log.node?.name && (
+ <>
+ [{log.node.name}]
+ {" "}
+ >
+ )}
{log.message}
))}
diff --git a/src/server/routers/pipeline.ts b/src/server/routers/pipeline.ts
index 4841761..346110c 100644
--- a/src/server/routers/pipeline.ts
+++ b/src/server/routers/pipeline.ts
@@ -822,11 +822,12 @@ export const pipelineRouter = router({
limit: z.number().min(1).max(500).default(200),
levels: z.array(z.nativeEnum(LogLevel)).optional(),
nodeId: z.string().optional(),
+ since: z.date().optional(),
}),
)
.use(withTeamAccess("VIEWER"))
.query(async ({ input }) => {
- const { pipelineId, cursor, limit, levels, nodeId } = input;
+ const { pipelineId, cursor, limit, levels, nodeId, since } = input;
const take = limit;
const where: Record = { pipelineId };
@@ -836,6 +837,9 @@ export const pipelineRouter = router({
if (nodeId) {
where.nodeId = nodeId;
}
+ if (since) {
+ where.timestamp = { gte: since };
+ }
const items = await prisma.pipelineLog.findMany({
where,