From 4186c6e20823038d5d528c879b136d48e24c00db Mon Sep 17 00:00:00 2001 From: jlightner Date: Mon, 30 Mar 2026 19:34:11 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Added=20DebugModeToggle=20component=20a?= =?UTF-8?q?nd=20StatusFilter=20pill=20bar=20to=20Admi=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "frontend/src/pages/AdminPipeline.tsx" - "frontend/src/api/public-client.ts" - "frontend/src/App.css" GSD-Task: S04/T01 --- frontend/src/App.css | 51 +++++++++++++ frontend/src/api/public-client.ts | 17 +++++ frontend/src/pages/AdminPipeline.tsx | 104 ++++++++++++++++++++++++++- 3 files changed, 170 insertions(+), 2 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index ba54afb..c641cf9 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -541,6 +541,57 @@ a.app-footer__repo:hover { cursor: not-allowed; } +/* ── Debug Mode Toggle ────────────────────────────────────────────────────── */ + +.debug-toggle { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.8125rem; +} + +.debug-toggle__label { + color: var(--color-text-on-header-label); + white-space: nowrap; +} + +.debug-toggle__switch { + position: relative; + width: 2.5rem; + height: 1.25rem; + background: var(--color-toggle-track); + border: none; + border-radius: 9999px; + cursor: pointer; + transition: background 0.2s; + flex-shrink: 0; +} + +.debug-toggle__switch--active { + background: var(--color-toggle-review); +} + +.debug-toggle__switch::after { + content: ""; + position: absolute; + top: 0.125rem; + left: 0.125rem; + width: 1rem; + height: 1rem; + background: var(--color-toggle-thumb); + border-radius: 50%; + transition: transform 0.2s; +} + +.debug-toggle__switch--active::after { + transform: translateX(1.25rem); +} + +.debug-toggle__switch:disabled { + opacity: 0.5; + cursor: not-allowed; +} + /* ── Pagination ───────────────────────────────────────────────────────────── */ .pagination { diff --git a/frontend/src/api/public-client.ts b/frontend/src/api/public-client.ts index 4f6817b..8e9133f 100644 --- a/frontend/src/api/public-client.ts +++ b/frontend/src/api/public-client.ts @@ -471,3 +471,20 @@ export async function revokePipeline(videoId: string): Promise { method: "POST", }); } + +// ── Debug Mode ────────────────────────────────────────────────────────────── + +export interface DebugModeResponse { + debug_mode: boolean; +} + +export async function fetchDebugMode(): Promise { + return request(`${BASE}/admin/pipeline/debug-mode`); +} + +export async function setDebugMode(enabled: boolean): Promise { + return request(`${BASE}/admin/pipeline/debug-mode`, { + method: "PUT", + body: JSON.stringify({ debug_mode: enabled }), + }); +} diff --git a/frontend/src/pages/AdminPipeline.tsx b/frontend/src/pages/AdminPipeline.tsx index 2a1ac72..f8f9c73 100644 --- a/frontend/src/pages/AdminPipeline.tsx +++ b/frontend/src/pages/AdminPipeline.tsx @@ -8,6 +8,8 @@ import { fetchPipelineVideos, fetchPipelineEvents, fetchWorkerStatus, + fetchDebugMode, + setDebugMode, triggerPipeline, revokePipeline, type PipelineVideoItem, @@ -364,6 +366,93 @@ function WorkerStatus() { ); } +// ── Debug Mode Toggle ──────────────────────────────────────────────────────── + +function DebugModeToggle() { + const [debugMode, setDebugModeState] = useState(null); + const [debugLoading, setDebugLoading] = useState(false); + + useEffect(() => { + let cancelled = false; + fetchDebugMode() + .then((res) => { + if (!cancelled) setDebugModeState(res.debug_mode); + }) + .catch(() => { + // silently fail — toggle stays hidden + }); + return () => { cancelled = true; }; + }, []); + + async function handleToggle() { + if (debugMode === null || debugLoading) return; + setDebugLoading(true); + try { + const res = await setDebugMode(!debugMode); + setDebugModeState(res.debug_mode); + } catch { + // swallow — leave previous state + } finally { + setDebugLoading(false); + } + } + + if (debugMode === null) return null; + + return ( +
+ + Debug {debugMode ? "On" : "Off"} + +
+ ); +} + +// ── Status Filter ──────────────────────────────────────────────────────────── + +function StatusFilter({ + videos, + activeFilter, + onFilterChange, +}: { + videos: PipelineVideoItem[]; + activeFilter: string | null; + onFilterChange: (filter: string | null) => void; +}) { + const statuses = Array.from(new Set(videos.map((v) => v.processing_status))).sort(); + + if (statuses.length <= 1) return null; + + return ( +
+ + {statuses.map((status) => ( + + ))} +
+ ); +} + // ── Main Page ──────────────────────────────────────────────────────────────── export default function AdminPipeline() { @@ -373,6 +462,7 @@ export default function AdminPipeline() { const [expandedId, setExpandedId] = useState(null); const [actionLoading, setActionLoading] = useState(null); const [actionMessage, setActionMessage] = useState<{ id: string; text: string; ok: boolean } | null>(null); + const [activeFilter, setActiveFilter] = useState(null); const load = useCallback(async () => { setLoading(true); @@ -448,6 +538,7 @@ export default function AdminPipeline() {

+