From 1138309aaa6b2902b881df2196cabdeaa81b01a2 Mon Sep 17 00:00:00 2001 From: Mondo Diaz Date: Mon, 2 Feb 2026 13:50:45 -0600 Subject: [PATCH] Add Active Workers table to Background Jobs dashboard Shows currently processing cache tasks in a dynamic table with: - Package name and version constraint being cached - Recursion depth and attempt number - Start timestamp - Pulsing indicator to show live activity Backend changes: - Add get_active_tasks() function to pypi_cache_worker.py - Add GET /pypi/cache/active endpoint to pypi_proxy.py Frontend changes: - Add PyPICacheActiveTask type - Add getPyPICacheActiveTasks() API function - Add Active Workers section with animated table - Auto-refreshes every 5 seconds with existing data --- backend/app/pypi_cache_worker.py | 32 ++++++ backend/app/pypi_proxy.py | 20 ++++ frontend/src/api.ts | 8 ++ frontend/src/pages/AdminJobsPage.css | 56 ++++++++++ frontend/src/pages/AdminJobsPage.tsx | 147 ++++++++++++++++++--------- frontend/src/types.ts | 9 ++ 6 files changed, 225 insertions(+), 47 deletions(-) diff --git a/backend/app/pypi_cache_worker.py b/backend/app/pypi_cache_worker.py index ceb6628..7776aa1 100644 --- a/backend/app/pypi_cache_worker.py +++ b/backend/app/pypi_cache_worker.py @@ -526,6 +526,38 @@ def get_failed_tasks(db: Session, limit: int = 50) -> List[dict]: ] +def get_active_tasks(db: Session, limit: int = 50) -> List[dict]: + """ + Get list of currently active (in_progress) tasks. + + Args: + db: Database session. + limit: Maximum number of tasks to return. + + Returns: + List of active task info dicts. + """ + tasks = ( + db.query(PyPICacheTask) + .filter(PyPICacheTask.status == "in_progress") + .order_by(PyPICacheTask.started_at.desc()) + .limit(limit) + .all() + ) + + return [ + { + "id": str(task.id), + "package": task.package_name, + "version_constraint": task.version_constraint, + "depth": task.depth, + "attempts": task.attempts, + "started_at": task.started_at.isoformat() if task.started_at else None, + } + for task in tasks + ] + + def retry_failed_task(db: Session, package_name: str) -> Optional[PyPICacheTask]: """ Reset a failed task to retry. diff --git a/backend/app/pypi_proxy.py b/backend/app/pypi_proxy.py index 2a117e5..a6cc35c 100644 --- a/backend/app/pypi_proxy.py +++ b/backend/app/pypi_proxy.py @@ -28,6 +28,7 @@ from .pypi_cache_worker import ( enqueue_cache_task, get_cache_status, get_failed_tasks, + get_active_tasks, retry_failed_task, retry_all_failed_tasks, ) @@ -849,6 +850,25 @@ async def pypi_cache_failed( return get_failed_tasks(db, limit=limit) +@router.get("/cache/active") +async def pypi_cache_active( + limit: int = Query(default=50, ge=1, le=500), + db: Session = Depends(get_db), + _current_user: User = Depends(require_admin), +): + """ + Get list of currently active (in_progress) cache tasks. + + Shows what the cache workers are currently processing. + + Args: + limit: Maximum number of tasks to return (default 50, max 500). + + Requires admin privileges. + """ + return get_active_tasks(db, limit=limit) + + @router.post("/cache/retry/{package_name}") async def pypi_cache_retry( package_name: str, diff --git a/frontend/src/api.ts b/frontend/src/api.ts index c6a3667..b079b48 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -754,6 +754,7 @@ export async function testUpstreamSource(id: string): Promise(response); } +export async function getPyPICacheActiveTasks(limit: number = 50): Promise { + const response = await fetch(`/pypi/cache/active?limit=${limit}`, { + credentials: 'include', + }); + return handleResponse(response); +} + export async function retryPyPICacheTask(packageName: string): Promise { const response = await fetch(`/pypi/cache/retry/${encodeURIComponent(packageName)}`, { method: 'POST', diff --git a/frontend/src/pages/AdminJobsPage.css b/frontend/src/pages/AdminJobsPage.css index 2aaf08d..cbfc94e 100644 --- a/frontend/src/pages/AdminJobsPage.css +++ b/frontend/src/pages/AdminJobsPage.css @@ -271,6 +271,62 @@ font-size: 0.8rem; } +/* Active Workers Section */ +.active-workers-section { + margin-bottom: 1.5rem; +} + +.active-workers-section h3 { + display: flex; + align-items: center; + gap: 0.5rem; + color: #3b82f6; + margin-top: 1rem; +} + +.pulse-indicator { + width: 8px; + height: 8px; + background-color: #3b82f6; + border-radius: 50%; + animation: pulse 1.5s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.5; + transform: scale(1.2); + } +} + +.jobs-table.active-table { + border: 1px solid #3b82f6; + border-radius: 4px; +} + +.jobs-table.active-table th { + background: #eff6ff; + color: #1d4ed8; +} + +.jobs-table .active-row { + background-color: #f0f9ff; +} + +.jobs-table .active-row:hover { + background-color: #e0f2fe; +} + +.jobs-table .version-constraint { + font-family: monospace; + font-size: 0.85rem; + color: var(--text-secondary); +} + /* Responsive */ @media (max-width: 768px) { .status-cards { diff --git a/frontend/src/pages/AdminJobsPage.tsx b/frontend/src/pages/AdminJobsPage.tsx index 2ef85b9..0a690da 100644 --- a/frontend/src/pages/AdminJobsPage.tsx +++ b/frontend/src/pages/AdminJobsPage.tsx @@ -4,10 +4,11 @@ import { useAuth } from '../contexts/AuthContext'; import { getPyPICacheStatus, getPyPICacheFailedTasks, + getPyPICacheActiveTasks, retryPyPICacheTask, retryAllPyPICacheTasks, } from '../api'; -import { PyPICacheStatus, PyPICacheTask } from '../types'; +import { PyPICacheStatus, PyPICacheTask, PyPICacheActiveTask } from '../types'; import './AdminJobsPage.css'; function AdminJobsPage() { @@ -17,6 +18,7 @@ function AdminJobsPage() { // PyPI cache status const [cacheStatus, setCacheStatus] = useState(null); const [failedTasks, setFailedTasks] = useState([]); + const [activeTasks, setActiveTasks] = useState([]); const [loadingStatus, setLoadingStatus] = useState(true); const [statusError, setStatusError] = useState(null); @@ -39,12 +41,14 @@ function AdminJobsPage() { setStatusError(null); try { - const [status, failed] = await Promise.all([ + const [status, failed, active] = await Promise.all([ getPyPICacheStatus(), getPyPICacheFailedTasks(100), + getPyPICacheActiveTasks(50), ]); setCacheStatus(status); setFailedTasks(failed); + setActiveTasks(active); } catch (err) { setStatusError(err instanceof Error ? err.message : 'Failed to load status'); } finally { @@ -192,52 +196,101 @@ function AdminJobsPage() { {totalJobs === 0 ? (

No cache jobs yet. Jobs are created when packages are downloaded through the PyPI proxy.

- ) : failedTasks.length === 0 && cacheStatus?.pending === 0 && cacheStatus?.in_progress === 0 ? ( -

All jobs completed successfully.

- ) : failedTasks.length > 0 ? ( - <> -

Failed Tasks

- - - - - - - - - - - - - {failedTasks.map((task) => ( - - - - - - - - - ))} - -
PackageErrorAttemptsDepthFailed AtActions
{task.package} - {task.error || 'Unknown error'} - {task.attempts}{task.depth} - {task.failed_at - ? new Date(task.failed_at).toLocaleString() - : '-'} - - -
- ) : ( -

Jobs are processing. No failures yet.

+ <> + {/* Active Workers Table */} + {activeTasks.length > 0 && ( +
+

+ + Active Workers ({activeTasks.length}) +

+ + + + + + + + + + + + {activeTasks.map((task) => ( + + + + + + + + ))} + +
PackageVersionDepthAttemptStarted
{task.package} + {task.version_constraint || '*'} + {task.depth}{task.attempts + 1} + {task.started_at + ? new Date(task.started_at).toLocaleTimeString() + : '-'} +
+
+ )} + + {/* Failed Tasks Table */} + {failedTasks.length > 0 && ( + <> +

Failed Tasks

+ + + + + + + + + + + + + {failedTasks.map((task) => ( + + + + + + + + + ))} + +
PackageErrorAttemptsDepthFailed AtActions
{task.package} + {task.error || 'Unknown error'} + {task.attempts}{task.depth} + {task.failed_at + ? new Date(task.failed_at).toLocaleString() + : '-'} + + +
+ + )} + + {/* Success message when nothing is pending/in-progress/failed */} + {failedTasks.length === 0 && activeTasks.length === 0 && cacheStatus?.pending === 0 && ( +

All jobs completed successfully.

+ )} + + {/* In progress message */} + {activeTasks.length === 0 && failedTasks.length === 0 && (cacheStatus?.pending ?? 0) > 0 && ( +

Jobs queued for processing...

+ )} + )} )} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index f06f8d0..8902dc5 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -575,6 +575,15 @@ export interface PyPICacheTask { failed_at: string | null; } +export interface PyPICacheActiveTask { + id: string; + package: string; + version_constraint: string | null; + depth: number; + attempts: number; + started_at: string | null; +} + export interface PyPICacheRetryResponse { message: string; task_id?: string;