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) <noreply@anthropic.com>
This commit is contained in:
jlightner 2026-03-31 22:10:07 -05:00
parent e80094dc05
commit 9b2db11095
2 changed files with 59 additions and 17 deletions

View file

@ -3689,6 +3689,22 @@ a.app-footer__repo:hover {
.stage-timeline__step--done + .stage-timeline__step::before { .stage-timeline__step--done + .stage-timeline__step::before {
background: #00c853; 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 ─────────────────────────────────────────────────── */ /* ── Recent Activity Feed ─────────────────────────────────────────────────── */

View file

@ -3,7 +3,7 @@
* expandable event log with token usage and collapsible JSON viewer. * 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 { useSearchParams } from "react-router-dom";
import { useDocumentTitle } from "../hooks/useDocumentTitle"; import { useDocumentTitle } from "../hooks/useDocumentTitle";
import { import {
@ -45,6 +45,19 @@ function formatTokens(n: number): string {
return String(n); 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<string, string> = { const STATUS_LABELS: Record<string, string> = {
not_started: "Not Started", not_started: "Not Started",
queued: "Queued", queued: "Queued",
@ -343,7 +356,7 @@ function EventLog({ videoId, status, runId }: { videoId: string; status: string;
// ── Worker Status ──────────────────────────────────────────────────────────── // ── Worker Status ────────────────────────────────────────────────────────────
function WorkerStatus() { const WorkerStatus = React.memo(function WorkerStatus() {
const [status, setStatus] = useState<WorkerStatusResponse | null>(null); const [status, setStatus] = useState<WorkerStatusResponse | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -397,7 +410,7 @@ function WorkerStatus() {
))} ))}
</div> </div>
); );
} });
// ── Debug Mode Toggle ──────────────────────────────────────────────────────── // ── Debug Mode Toggle ────────────────────────────────────────────────────────
@ -443,7 +456,7 @@ function DebugModeToggle({
// ── Status Filter ──────────────────────────────────────────────────────────── // ── Status Filter ────────────────────────────────────────────────────────────
function StatusFilter({ const StatusFilter = React.memo(function StatusFilter({
videos, videos,
activeFilter, activeFilter,
onFilterChange, onFilterChange,
@ -483,7 +496,7 @@ function StatusFilter({
})} })}
</div> </div>
); );
} });
// ── Run List ───────────────────────────────────────────────────────────────── // ── Run List ─────────────────────────────────────────────────────────────────
@ -614,15 +627,23 @@ const PIPELINE_STAGES = [
{ key: "stage6_embed", label: "Embed" }, { key: "stage6_embed", label: "Embed" },
]; ];
function StageTimeline({ video }: { video: PipelineVideoItem }) { const StageTimeline = React.memo(function StageTimeline({ video }: { video: PipelineVideoItem }) {
if (video.processing_status !== "processing" && video.processing_status !== "complete" && video.processing_status !== "error") { 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; return null;
} }
const activeStage = video.active_stage; const activeStage = video.active_stage;
const activeStatus = video.active_stage_status; // "start" = running, "complete" = done, "error" = failed const activeStatus = video.active_stage_status;
// Determine each stage's state
const activeIdx = PIPELINE_STAGES.findIndex((s) => s.key === activeStage); const activeIdx = PIPELINE_STAGES.findIndex((s) => s.key === activeStage);
return ( return (
@ -638,7 +659,6 @@ function StageTimeline({ video }: { video: PipelineVideoItem }) {
else if (activeStatus === "error") stateClass = "stage-timeline__step--error"; else if (activeStatus === "error") stateClass = "stage-timeline__step--error";
} }
} }
// If video is complete, all stages are done
if (video.processing_status === "complete") { if (video.processing_status === "complete") {
stateClass = "stage-timeline__step--done"; stateClass = "stage-timeline__step--done";
} }
@ -650,13 +670,19 @@ function StageTimeline({ video }: { video: PipelineVideoItem }) {
</div> </div>
); );
})} })}
{isProcessing && activeStatus === "start" && (
<span className="stage-timeline__elapsed">{formatElapsed(video.stage_started_at)}</span>
)}
{isProcessing && video.latest_run && video.latest_run.total_tokens > 0 && (
<span className="stage-timeline__tokens">{formatTokens(video.latest_run.total_tokens)} tok</span>
)}
</div> </div>
); );
} })
// ── Recent Activity Feed ───────────────────────────────────────────────────── // ── Recent Activity Feed ─────────────────────────────────────────────────────
function RecentActivityFeed() { const RecentActivityFeed = React.memo(function RecentActivityFeed() {
const [items, setItems] = useState<RecentActivityItem[]>([]); const [items, setItems] = useState<RecentActivityItem[]>([]);
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -718,7 +744,7 @@ function RecentActivityFeed() {
)} )}
</div> </div>
); );
} });
// ── Main Page ──────────────────────────────────────────────────────────────── // ── Main Page ────────────────────────────────────────────────────────────────
@ -760,8 +786,8 @@ export default function AdminPipeline() {
const videoRefs = useRef<Map<string, HTMLDivElement>>(new Map()); const videoRefs = useRef<Map<string, HTMLDivElement>>(new Map());
const deepLinked = useRef(false); const deepLinked = useRef(false);
// Compute filtered list (status + creator) // Compute filtered list (status + creator) — memoized to avoid recomputing on unrelated state changes
const filteredVideos = videos.filter((v) => { const filteredVideos = useMemo(() => videos.filter((v) => {
if (activeFilter !== null && v.processing_status !== activeFilter) return false; if (activeFilter !== null && v.processing_status !== activeFilter) return false;
if (creatorFilter !== null && v.creator_name !== creatorFilter) return false; if (creatorFilter !== null && v.creator_name !== creatorFilter) return false;
if (searchQuery) { if (searchQuery) {
@ -769,7 +795,7 @@ export default function AdminPipeline() {
if (!v.filename.toLowerCase().includes(q) && !v.creator_name.toLowerCase().includes(q)) return false; if (!v.filename.toLowerCase().includes(q) && !v.creator_name.toLowerCase().includes(q)) return false;
} }
return true; return true;
}); }), [videos, activeFilter, creatorFilter, searchQuery]);
const load = useCallback(async (isAutoRefresh = false) => { const load = useCallback(async (isAutoRefresh = false) => {
if (!isAutoRefresh) setLoading(true); if (!isAutoRefresh) setLoading(true);