Background pipeline status: registry + /api/graph/status + graph-view chips
The background extraction pipeline is invisible on the home surface — essential observability per the user's directive: the graph view must show what is currently running (extracting / auditing / merging) and for which chat. - extraction.ts: module-level registry of in-flight runs (chatId, phase, startedAt), updated at each phase transition in runExtraction and cleared in finally (done or failed). extractionStatuses() is the read; one process, one registry. - GET /api/graph/status: thin transport over the registry, chat titles joined from the chat store. - graph-view: polls every 2s and shows one chip per running pipeline (spinner + phase + title + elapsed seconds, bottom-left); when a run finishes (some -> none) the graph refetches, so merges land live. Status is cosmetic — poll errors never surface. - Unit test drives a mocked pipeline run and observes the registry mid-flight: 'extracting' during the extractor's generate, 'auditing' during the auditor's, empty after.
This commit is contained in:
@@ -56,7 +56,7 @@
|
||||
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { SearchIcon, WaypointsIcon } from "lucide-react";
|
||||
import { Loader2Icon, SearchIcon, WaypointsIcon } from "lucide-react";
|
||||
import Graph from "graphology";
|
||||
import forceAtlas2 from "graphology-layout-forceatlas2";
|
||||
import type { NodeHoverDrawingFunction, NodeLabelDrawingFunction } from "sigma/rendering";
|
||||
@@ -352,7 +352,9 @@ export function GraphView() {
|
||||
return set;
|
||||
}, [snapshot, query]);
|
||||
|
||||
// Fetch the derived graph (the route folds stale chats first).
|
||||
// Fetch the derived graph. Re-runs when a background pipeline run
|
||||
// finishes (refreshTick) — its merges just landed in the ledger.
|
||||
const [refreshTick, setRefreshTick] = useState(0);
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
fetch("/api/graph")
|
||||
@@ -364,6 +366,30 @@ export function GraphView() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [refreshTick]);
|
||||
|
||||
// Background pipeline status: poll /api/graph/status; when a run
|
||||
// finishes (some -> none), refetch the graph.
|
||||
const [pipelineRuns, setPipelineRuns] = useState<{ chatId: string; title: string; phase: string; startedAt: number }[]>([]);
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let hadRuns = false;
|
||||
const poll = () =>
|
||||
fetch("/api/graph/status")
|
||||
.then((res) => res.json() as Promise<{ runs: typeof pipelineRuns }>)
|
||||
.then(({ runs }) => {
|
||||
if (cancelled) return;
|
||||
setPipelineRuns(runs);
|
||||
if (hadRuns && runs.length === 0) setRefreshTick((t) => t + 1);
|
||||
hadRuns = runs.length > 0;
|
||||
})
|
||||
.catch(() => {}); // status is cosmetic — never surface poll errors
|
||||
poll();
|
||||
const timer = setInterval(poll, 2000);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Build the graphology graph, lay it out once, mount sigma.
|
||||
@@ -612,6 +638,23 @@ export function GraphView() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Background pipeline status: what is currently running. */}
|
||||
{pipelineRuns.length > 0 && (
|
||||
<div className="absolute bottom-3 left-3 z-10 flex flex-col gap-1.5">
|
||||
{pipelineRuns.map((run) => (
|
||||
<div
|
||||
key={run.chatId}
|
||||
className="flex items-center gap-2 rounded-full border bg-card/85 px-3 py-1.5 text-xs text-muted-foreground shadow-lg backdrop-blur"
|
||||
>
|
||||
<Loader2Icon className="size-3.5 shrink-0 animate-spin" />
|
||||
<span className="capitalize">{run.phase}</span>
|
||||
<span className="max-w-48 truncate text-foreground">{run.title}</span>
|
||||
<span className="tabular-nums">{Math.max(0, Math.round((Date.now() - run.startedAt) / 1000))}s</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selected && !isEmpty && (
|
||||
<div
|
||||
ref={cardRef}
|
||||
|
||||
Reference in New Issue
Block a user