From 00ab23b19c33286313a9d2f2170508b8027e206b Mon Sep 17 00:00:00 2001
From: ABHIJEET <71241536+abhijeetunreal@users.noreply.github.com>
Date: Sun, 7 Jun 2026 17:52:06 +0530
Subject: [PATCH 1/7] hi
---
web/app/api/jobs/[id]/cancel/route.ts | 32 ++
.../components/pipeline/PipelineRunPanel.tsx | 39 +-
.../pipeline/PipelineRunPreviewCard.tsx | 146 ++++++
.../components/pipeline/PipelineRunnerFab.tsx | 18 +-
.../pipeline/PipelineSettingsPanel.tsx | 439 ++++++++++--------
web/src/components/pipeline/pipelineUi.tsx | 33 ++
web/src/context/PipelineContext.tsx | 49 ++
web/src/lib/pipelineRunPreview.test.ts | 37 ++
web/src/lib/pipelineRunPreview.ts | 287 ++++++++++++
web/src/server/jobsCancelRoute.test.ts | 48 ++
web/src/server/pipelineJobs.ts | 130 +++++-
web/src/server/pipelineJobsDb.ts | 19 +
web/src/strings.json | 16 +
web/src/types/api.ts | 9 +-
web/src/views/Pipeline.tsx | 20 +-
15 files changed, 1097 insertions(+), 225 deletions(-)
create mode 100644 web/app/api/jobs/[id]/cancel/route.ts
create mode 100644 web/src/components/pipeline/PipelineRunPreviewCard.tsx
create mode 100644 web/src/lib/pipelineRunPreview.test.ts
create mode 100644 web/src/lib/pipelineRunPreview.ts
create mode 100644 web/src/server/jobsCancelRoute.test.ts
diff --git a/web/app/api/jobs/[id]/cancel/route.ts b/web/app/api/jobs/[id]/cancel/route.ts
new file mode 100644
index 00000000..44f99111
--- /dev/null
+++ b/web/app/api/jobs/[id]/cancel/route.ts
@@ -0,0 +1,32 @@
+import { NextResponse, type NextRequest } from 'next/server';
+import { forbiddenIfNotLocal } from '@/server/localOnly';
+import { requireApiAuth } from '@/server/auth';
+import { cancelPipelineJob } from '@/server/pipelineJobs';
+import type { ApiRouteHandlerWithParams } from '@/types/api';
+
+export const runtime = 'nodejs';
+
+/**
+ * POST /api/jobs/:id/cancel — stop a running pipeline job.
+ */
+export const POST: ApiRouteHandlerWithParams<{ id: string }> = async (
+ request: NextRequest,
+ { params }: { params: Promise<{ id: string }> },
+): Promise => {
+ const denied = forbiddenIfNotLocal(request);
+ if (denied) return denied;
+ const authDenied = requireApiAuth(request);
+ if (authDenied) return authDenied;
+
+ const { id } = await params;
+ const result = await cancelPipelineJob(id);
+ if (!result.ok) {
+ const status = result.error === 'Job not found' ? 404 : 409;
+ return NextResponse.json({ error: result.error || 'Unable to cancel job' }, { status });
+ }
+ return NextResponse.json({
+ ok: true,
+ status: result.status,
+ error: result.error ?? null,
+ });
+};
diff --git a/web/src/components/pipeline/PipelineRunPanel.tsx b/web/src/components/pipeline/PipelineRunPanel.tsx
index f728409e..8e228b29 100644
--- a/web/src/components/pipeline/PipelineRunPanel.tsx
+++ b/web/src/components/pipeline/PipelineRunPanel.tsx
@@ -19,6 +19,7 @@ import { usePipeline } from '@/context/PipelineContext';
import { useReadOnlySession } from '@/hooks/useReadOnlySession';
import {
PipelineStatusBadge,
+ PipelineStopButton,
PRESET_COPY,
PresetIcon,
} from './pipelineUi';
@@ -27,6 +28,7 @@ import { CRAWL_PRESETS, type CrawlPresetId } from '@/lib/crawlPresets';
import PipelineWizardProgress, { type WizardStep } from './PipelineWizardProgress';
import PipelineLogViewer from './PipelineLogViewer';
import CrawlAuthorizeCheckbox from './CrawlAuthorizeCheckbox';
+import PipelineRunPreviewCard from './PipelineRunPreviewCard';
const s = strings.pipelineRunner;
const crawlPresets = s.crawlPresets as Record;
@@ -56,6 +58,8 @@ export default function PipelineRunPanel() {
status,
log,
startUrl,
+ configState,
+ customCommand,
presetId,
handleStartUrlChange,
handlePresetChange,
@@ -63,6 +67,8 @@ export default function PipelineRunPanel() {
handleCrawlPresetChange,
setField,
run,
+ cancelJob,
+ stopping,
continueInBackground,
} = usePipeline();
const { readOnly } = useReadOnlySession();
@@ -318,6 +324,13 @@ export default function PipelineRunPanel() {
+
+
{busy ? (
-
+ <>
+
+
+ >
) : null}
{s.outputLabel}
-
+
+ {busy ? (
+
+ ) : null}
+
+
{log ? (
diff --git a/web/src/components/pipeline/PipelineRunPreviewCard.tsx b/web/src/components/pipeline/PipelineRunPreviewCard.tsx
new file mode 100644
index 00000000..75f19014
--- /dev/null
+++ b/web/src/components/pipeline/PipelineRunPreviewCard.tsx
@@ -0,0 +1,146 @@
+'use client';
+
+import { useState } from 'react';
+import { ChevronDown, ChevronUp, Clock, Layers, Settings2 } from 'lucide-react';
+import { strings } from '@/lib/strings';
+import {
+ buildPipelineRunPreview,
+ formatPipelineRunDuration,
+} from '@/lib/pipelineRunPreview';
+import type { PipelinePresetId } from '@/components/pipeline/pipelinePresets';
+import type { CrawlPresetId } from '@/lib/crawlPresets';
+import type { PipelineConfigState } from '@/types/api';
+
+const s = strings.pipelineRunner.runPreview;
+
+export interface PipelineRunPreviewCardProps {
+ presetId: PipelinePresetId;
+ configState: PipelineConfigState;
+ customCommand?: string;
+ crawlPresetId?: CrawlPresetId | '';
+}
+
+export default function PipelineRunPreviewCard({
+ presetId,
+ configState,
+ customCommand = '',
+ crawlPresetId = '',
+}: PipelineRunPreviewCardProps) {
+ const [configOpen, setConfigOpen] = useState(false);
+ const preview = buildPipelineRunPreview({
+ presetId,
+ configState,
+ customCommand,
+ crawlPresetId,
+ });
+
+ const duration = formatPipelineRunDuration(preview.timeMinSeconds, preview.timeMaxSeconds);
+
+ return (
+
+
+
+
+
+ {s.estimatedTime}: {duration}
+
+
+
+
+
+
+ {s.maxPagesLabel}
+
+
+ {preview.maxCrawlPages != null ? preview.maxCrawlPages.toLocaleString() : '—'}
+
+
+
+
+ {s.lighthousePagesLabel}
+
+
+ {preview.lighthousePages != null ? preview.lighthousePages.toLocaleString() : '—'}
+
+
+
+
+ {s.stepsLabel}
+
+
+ {preview.phases.length}
+
+
+
+
+
+
+
+ {s.whatRunsLabel}
+
+
+ {preview.phases.map((phase) => (
+ -
+
+
+
{phase.label}
+ {phase.detail ? (
+
{phase.detail}
+ ) : null}
+
+
+ ))}
+
+
+
+
+ {preview.summaryLines.map((line) => (
+ - {line}
+ ))}
+ - {s.estimateDisclaimer}
+
+
+ {preview.configRows.length > 0 ? (
+
+
+ {configOpen ? (
+
+ {preview.configRows.map((row) => (
+
+
-
+ {row.label}
+
+ - {row.value}
+
+ ))}
+
+ ) : null}
+
+ ) : null}
+
+ );
+}
diff --git a/web/src/components/pipeline/PipelineRunnerFab.tsx b/web/src/components/pipeline/PipelineRunnerFab.tsx
index 3c216848..1f8e47ba 100644
--- a/web/src/components/pipeline/PipelineRunnerFab.tsx
+++ b/web/src/components/pipeline/PipelineRunnerFab.tsx
@@ -1,6 +1,6 @@
'use client';
-import { Loader2, Maximize2, Terminal } from 'lucide-react';
+import { Loader2, Maximize2, Square, Terminal } from 'lucide-react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { strings } from '@/lib/strings';
import { usePipeline } from '@/context/PipelineContext';
@@ -16,7 +16,7 @@ export default function PipelineRunnerFab() {
const pathname = usePathname();
const router = useRouter();
const searchParams = useSearchParams();
- const { busy, status, log, backgroundMode, openPipelinePage } = usePipeline();
+ const { busy, status, log, backgroundMode, stopping, cancelJob, openPipelinePage } = usePipeline();
const { loading: sessionLoading, canMutate } = useSession();
const onPipelinePage = pathname === '/pipeline' || pathname.startsWith('/pipeline/');
@@ -66,6 +66,20 @@ export default function PipelineRunnerFab() {
: 'Idle'}
+