From 9b2db11095e27ace2d88376dd0fb847458fab0b6 Mon Sep 17 00:00:00 2001 From: jlightner Date: Tue, 31 Mar 2026 22:10:07 -0500 Subject: [PATCH] feat: Enrich in-progress stage display and memoize pipeline page In-progress stages now show: - Live elapsed time (ticks every second) next to the active stage dot - Run-level token count so far Performance: wrapped StageTimeline, StatusFilter, WorkerStatus, and RecentActivityFeed with React.memo. Memoized filteredVideos with useMemo. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/App.css | 16 ++++++++ frontend/src/pages/AdminPipeline.tsx | 60 ++++++++++++++++++++-------- 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index a6c739f..d375724 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -3689,6 +3689,22 @@ a.app-footer__repo:hover { .stage-timeline__step--done + .stage-timeline__step::before { background: #00c853; } +.stage-timeline__elapsed { + font-size: 0.7rem; + font-weight: 600; + color: var(--color-accent); + margin-left: 6px; + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.stage-timeline__tokens { + font-size: 0.65rem; + color: var(--color-text-muted); + margin-left: 4px; + white-space: nowrap; +} + /* ── Recent Activity Feed ─────────────────────────────────────────────────── */ diff --git a/frontend/src/pages/AdminPipeline.tsx b/frontend/src/pages/AdminPipeline.tsx index a92dc9f..1c4f1e2 100644 --- a/frontend/src/pages/AdminPipeline.tsx +++ b/frontend/src/pages/AdminPipeline.tsx @@ -3,7 +3,7 @@ * expandable event log with token usage and collapsible JSON viewer. */ -import { useCallback, useEffect, useRef, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { useDocumentTitle } from "../hooks/useDocumentTitle"; import { @@ -45,6 +45,19 @@ function formatTokens(n: number): string { return String(n); } + +function formatElapsed(isoStart: string | null): string { + if (!isoStart) return ""; + const ms = Date.now() - new Date(isoStart).getTime(); + if (ms < 0) return ""; + const secs = Math.floor(ms / 1000); + if (secs < 60) return `${secs}s`; + const mins = Math.floor(secs / 60); + const remainSecs = secs % 60; + if (mins < 60) return `${mins}m ${remainSecs}s`; + const hrs = Math.floor(mins / 60); + return `${hrs}h ${mins % 60}m`; +} const STATUS_LABELS: Record = { not_started: "Not Started", queued: "Queued", @@ -343,7 +356,7 @@ function EventLog({ videoId, status, runId }: { videoId: string; status: string; // ── Worker Status ──────────────────────────────────────────────────────────── -function WorkerStatus() { +const WorkerStatus = React.memo(function WorkerStatus() { const [status, setStatus] = useState(null); const [error, setError] = useState(null); @@ -397,7 +410,7 @@ function WorkerStatus() { ))} ); -} +}); // ── Debug Mode Toggle ──────────────────────────────────────────────────────── @@ -443,7 +456,7 @@ function DebugModeToggle({ // ── Status Filter ──────────────────────────────────────────────────────────── -function StatusFilter({ +const StatusFilter = React.memo(function StatusFilter({ videos, activeFilter, onFilterChange, @@ -483,7 +496,7 @@ function StatusFilter({ })} ); -} +}); // ── Run List ───────────────────────────────────────────────────────────────── @@ -614,15 +627,23 @@ const PIPELINE_STAGES = [ { key: "stage6_embed", label: "Embed" }, ]; -function StageTimeline({ video }: { video: PipelineVideoItem }) { - if (video.processing_status !== "processing" && video.processing_status !== "complete" && video.processing_status !== "error") { +const StageTimeline = React.memo(function StageTimeline({ video }: { video: PipelineVideoItem }) { + const [, setTick] = useState(0); + const isProcessing = video.processing_status === "processing"; + + // Tick every second while processing to update elapsed time + useEffect(() => { + if (!isProcessing) return; + const id = setInterval(() => setTick((t) => t + 1), 1000); + return () => clearInterval(id); + }, [isProcessing]); + + if (!isProcessing && video.processing_status !== "complete" && video.processing_status !== "error") { return null; } const activeStage = video.active_stage; - const activeStatus = video.active_stage_status; // "start" = running, "complete" = done, "error" = failed - - // Determine each stage's state + const activeStatus = video.active_stage_status; const activeIdx = PIPELINE_STAGES.findIndex((s) => s.key === activeStage); return ( @@ -638,7 +659,6 @@ function StageTimeline({ video }: { video: PipelineVideoItem }) { else if (activeStatus === "error") stateClass = "stage-timeline__step--error"; } } - // If video is complete, all stages are done if (video.processing_status === "complete") { stateClass = "stage-timeline__step--done"; } @@ -650,13 +670,19 @@ function StageTimeline({ video }: { video: PipelineVideoItem }) { ); })} + {isProcessing && activeStatus === "start" && ( + {formatElapsed(video.stage_started_at)} + )} + {isProcessing && video.latest_run && video.latest_run.total_tokens > 0 && ( + {formatTokens(video.latest_run.total_tokens)} tok + )} ); -} +}) // ── Recent Activity Feed ───────────────────────────────────────────────────── -function RecentActivityFeed() { +const RecentActivityFeed = React.memo(function RecentActivityFeed() { const [items, setItems] = useState([]); const [collapsed, setCollapsed] = useState(false); const [loading, setLoading] = useState(true); @@ -718,7 +744,7 @@ function RecentActivityFeed() { )} ); -} +}); // ── Main Page ──────────────────────────────────────────────────────────────── @@ -760,8 +786,8 @@ export default function AdminPipeline() { const videoRefs = useRef>(new Map()); const deepLinked = useRef(false); - // Compute filtered list (status + creator) - const filteredVideos = videos.filter((v) => { + // Compute filtered list (status + creator) — memoized to avoid recomputing on unrelated state changes + const filteredVideos = useMemo(() => videos.filter((v) => { if (activeFilter !== null && v.processing_status !== activeFilter) return false; if (creatorFilter !== null && v.creator_name !== creatorFilter) return false; if (searchQuery) { @@ -769,7 +795,7 @@ export default function AdminPipeline() { if (!v.filename.toLowerCase().includes(q) && !v.creator_name.toLowerCase().includes(q)) return false; } return true; - }); + }), [videos, activeFilter, creatorFilter, searchQuery]); const load = useCallback(async (isAutoRefresh = false) => { if (!isAutoRefresh) setLoading(true);