Wire frontend to real API endpoints

Dashboard page:
- Fetch top clients from /api/dashboard/by-client
- Show loading state while fetching
- Display empty state when no client data
- Show real client_id, job count, and success rate

Scrapers page:
- Fetch versions from /api/admin/scrapers
- Wire promote/deprecate buttons to real API calls
- Wire add version form to POST /api/admin/scrapers
- Wire traffic allocation to PUT /api/admin/scrapers/{id}/traffic
- Add loading and error states

Dockerfile:
- Add COPY commands for new directories (api/, core/, scrapers/, etc.)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-01-24 16:05:29 +00:00
parent 39c80fc8be
commit 3317553658
3 changed files with 277 additions and 145 deletions

View File

@@ -1,18 +1,23 @@
'use client';
import Link from 'next/link';
import { useState, useMemo } from 'react';
import { useState, useMemo, useEffect } from 'react';
import { useJobs } from '@/contexts/JobsContext';
import { JobStatus } from '@/components/ScraperTest';
// Mock data for initial development - will be replaced with API data
const MOCK_CLIENTS = [
{ client_id: 'client-001', job_count: 45, success_rate: 94.2 },
{ client_id: 'client-002', job_count: 38, success_rate: 89.5 },
{ client_id: 'client-003', job_count: 27, success_rate: 96.3 },
{ client_id: 'client-004', job_count: 19, success_rate: 84.2 },
{ client_id: 'client-005', job_count: 12, success_rate: 91.7 },
];
// API base URL
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
// Client stats type from API
interface ClientStats {
client_id: string;
source: string | null;
total_jobs: number;
completed: number;
failed: number;
success_rate: number;
total_reviews: number;
}
function formatDuration(seconds: number): string {
if (seconds < 60) return `${seconds.toFixed(1)}s`;
@@ -55,6 +60,26 @@ function getErrorType(errorMessage: string | null): string {
export default function DashboardPage() {
const { jobs, isLoading } = useJobs();
const [currentDate] = useState(new Date());
const [clients, setClients] = useState<ClientStats[]>([]);
const [clientsLoading, setClientsLoading] = useState(true);
// Fetch client stats from API
useEffect(() => {
async function fetchClients() {
try {
const response = await fetch(`${API_BASE}/api/dashboard/by-client?limit=5`);
if (response.ok) {
const data = await response.json();
setClients(data);
}
} catch (error) {
console.error('Failed to fetch client stats:', error);
} finally {
setClientsLoading(false);
}
}
fetchClients();
}, []);
// Calculate stats from jobs data
const stats = useMemo(() => {
@@ -453,42 +478,67 @@ export default function DashboardPage() {
View all
</Link>
</div>
<div className="space-y-3">
{MOCK_CLIENTS.map((client, index) => (
<div
key={client.client_id}
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
{clientsLoading ? (
<div className="py-8 flex justify-center">
<div className="w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
</div>
) : clients.length === 0 ? (
<div className="py-8 text-center text-gray-500">
<svg
className="w-12 h-12 mx-auto mb-3 opacity-30"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<div className="flex items-center gap-3">
<span className="w-6 h-6 bg-blue-100 text-blue-700 rounded-full flex items-center justify-center text-xs font-bold">
{index + 1}
</span>
<div>
<p className="font-medium text-gray-800">
{client.client_id}
</p>
<p className="text-xs text-gray-500">
{client.job_count} jobs
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
/>
</svg>
<p className="font-medium">No client data yet</p>
<p className="text-sm mt-1">Run jobs with requester metadata to see stats</p>
</div>
) : (
<div className="space-y-3">
{clients.map((client, index) => (
<div
key={client.client_id}
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
>
<div className="flex items-center gap-3">
<span className="w-6 h-6 bg-blue-100 text-blue-700 rounded-full flex items-center justify-center text-xs font-bold">
{index + 1}
</span>
<div>
<p className="font-medium text-gray-800">
{client.client_id}
</p>
<p className="text-xs text-gray-500">
{client.total_jobs} jobs
{client.source && ` · ${client.source}`}
</p>
</div>
</div>
<div className="text-right">
<p
className={`font-semibold ${
client.success_rate >= 90
? 'text-green-600'
: client.success_rate >= 80
? 'text-yellow-600'
: 'text-red-600'
}`}
>
{client.success_rate.toFixed(1)}%
</p>
<p className="text-xs text-gray-500">success</p>
</div>
</div>
<div className="text-right">
<p
className={`font-semibold ${
client.success_rate >= 90
? 'text-green-600'
: client.success_rate >= 80
? 'text-yellow-600'
: 'text-red-600'
}`}
>
{client.success_rate}%
</p>
<p className="text-xs text-gray-500">success</p>
</div>
</div>
))}
</div>
))}
</div>
)}
</div>
</div>