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() {
+