feat: deep-link Inspect Pipeline button — auto-expand and scroll to video

TechniquePage's 'Inspect pipeline' button passes ?video=<id> to AdminPipeline.
Previously the query param was ignored. Now AdminPipeline reads it on mount,
auto-expands the matching video row, and smooth-scrolls it into view.
This commit is contained in:
jlightner 2026-03-31 02:16:36 +00:00
parent 4b0914b12b
commit 4151e7cd25

View file

@ -3,7 +3,8 @@
* expandable event log with token usage and collapsible JSON viewer. * expandable event log with token usage and collapsible JSON viewer.
*/ */
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { useSearchParams } from "react-router-dom";
import { import {
fetchPipelineVideos, fetchPipelineVideos,
fetchPipelineEvents, fetchPipelineEvents,
@ -448,6 +449,7 @@ function StatusFilter({
// ── Main Page ──────────────────────────────────────────────────────────────── // ── Main Page ────────────────────────────────────────────────────────────────
export default function AdminPipeline() { export default function AdminPipeline() {
const [searchParams] = useSearchParams();
const [videos, setVideos] = useState<PipelineVideoItem[]>([]); const [videos, setVideos] = useState<PipelineVideoItem[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -456,6 +458,8 @@ export default function AdminPipeline() {
const [actionMessage, setActionMessage] = useState<{ id: string; text: string; ok: boolean } | null>(null); const [actionMessage, setActionMessage] = useState<{ id: string; text: string; ok: boolean } | null>(null);
const [activeFilter, setActiveFilter] = useState<string | null>(null); const [activeFilter, setActiveFilter] = useState<string | null>(null);
const [debugMode, setDebugModeState] = useState<boolean | null>(null); const [debugMode, setDebugModeState] = useState<boolean | null>(null);
const videoRefs = useRef<Map<string, HTMLDivElement>>(new Map());
const deepLinked = useRef(false);
const load = useCallback(async () => { const load = useCallback(async () => {
setLoading(true); setLoading(true);
@ -474,6 +478,22 @@ export default function AdminPipeline() {
void load(); void load();
}, [load]); }, [load]);
// Deep-link: auto-expand and scroll to ?video=<id> on first load
useEffect(() => {
if (deepLinked.current || loading || videos.length === 0) return;
const targetVideoId = searchParams.get("video");
if (!targetVideoId) return;
const match = videos.find((v) => v.id === targetVideoId);
if (!match) return;
deepLinked.current = true;
setExpandedId(targetVideoId);
// Scroll after the expanded detail renders
requestAnimationFrame(() => {
const el = videoRefs.current.get(targetVideoId);
if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
});
}, [loading, videos, searchParams]);
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
fetchDebugMode() fetchDebugMode()
@ -568,7 +588,7 @@ export default function AdminPipeline() {
{videos {videos
.filter((v) => activeFilter === null || v.processing_status === activeFilter) .filter((v) => activeFilter === null || v.processing_status === activeFilter)
.map((video) => ( .map((video) => (
<div key={video.id} className="pipeline-video"> <div key={video.id} className="pipeline-video" ref={(el) => { if (el) videoRefs.current.set(video.id, el); }}>
<div <div
className="pipeline-video__header" className="pipeline-video__header"
onClick={() => toggleExpand(video.id)} onClick={() => toggleExpand(video.id)}