diff --git a/backend/app/api/v1/jobs.py b/backend/app/api/v1/jobs.py index f6f85a1..0d2277a 100644 --- a/backend/app/api/v1/jobs.py +++ b/backend/app/api/v1/jobs.py @@ -2,9 +2,12 @@ from datetime import datetime from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query, status +from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.dependencies import get_current_user, get_db +from app.models.job import LocaleInstance as LocaleInstanceModel +from app.models.output import OutputRow, ConfidenceTier from app.schemas.common import PaginatedResponse from app.schemas.job import JobCreate, JobListResponse, JobResponse, JobUpdate, LocaleInstanceResponse from app.services.audit_service import AuditService @@ -68,8 +71,33 @@ async def list_jobs( page=page, page_size=page_size, ) + # Batch-load confidence counts for all jobs in one query + job_ids = [job.id for job in jobs] + confidence_map: dict[str, dict] = {} + if job_ids: + conf_query = ( + select( + LocaleInstanceModel.job_id, + OutputRow.confidence_tier, + func.count().label("cnt"), + ) + .join(OutputRow, OutputRow.instance_id == LocaleInstanceModel.id) + .where(LocaleInstanceModel.job_id.in_(job_ids)) + .group_by(LocaleInstanceModel.job_id, OutputRow.confidence_tier) + ) + conf_result = await db.execute(conf_query) + for row in conf_result.all(): + jid = str(row.job_id) + if jid not in confidence_map: + confidence_map[jid] = {"high": 0, "moderate": 0, "low": 0, "total": 0} + tier_val = row.confidence_tier.value if hasattr(row.confidence_tier, "value") else row.confidence_tier + confidence_map[jid][tier_val] = row.cnt + confidence_map[jid]["total"] += row.cnt + items = [] for job in jobs: + jid = str(job.id) + conf = confidence_map.get(jid, {}) item = JobListResponse( id=job.id, client_id=job.client_id, @@ -86,6 +114,10 @@ async def list_jobs( updated_at=job.updated_at, client_name=job.client.name if job.client else None, created_by_name=job.creator.name if job.creator else None, + confidence_high=conf.get("high", 0), + confidence_moderate=conf.get("moderate", 0), + confidence_low=conf.get("low", 0), + total_output_rows=conf.get("total", 0), ) items.append(item) diff --git a/backend/app/schemas/job.py b/backend/app/schemas/job.py index 9c429e1..a4da7e3 100644 --- a/backend/app/schemas/job.py +++ b/backend/app/schemas/job.py @@ -99,5 +99,10 @@ class JobListResponse(BaseModel): # Enrichment fields (populated by API layer) client_name: str | None = None created_by_name: str | None = None + # Confidence breakdown (populated by API layer) + confidence_high: int = 0 + confidence_moderate: int = 0 + confidence_low: int = 0 + total_output_rows: int = 0 model_config = {"from_attributes": True} diff --git a/frontend/src/components/jobs/JobCard.tsx b/frontend/src/components/jobs/JobCard.tsx index f04c630..71bda6f 100644 --- a/frontend/src/components/jobs/JobCard.tsx +++ b/frontend/src/components/jobs/JobCard.tsx @@ -7,7 +7,7 @@ import { Badge } from "@/components/ui/badge"; import type { Job } from "@/lib/types"; import { formatDate } from "@/lib/utils"; import { cn } from "@/lib/utils"; -import { Clock, User, FileText, DollarSign } from "lucide-react"; +import { Clock, User, DollarSign } from "lucide-react"; const statusConfig: Record< string, @@ -90,6 +90,46 @@ export function JobCard({ job }: JobCardProps) { ))} + + {/* Confidence breakdown */} + {(job.total_output_rows ?? 0) > 0 && ( +
+
+ {(job.confidence_high ?? 0) > 0 && ( +
+ )} + {(job.confidence_moderate ?? 0) > 0 && ( +
+ )} + {(job.confidence_low ?? 0) > 0 && ( +
+ )} +
+
+ + + {job.confidence_high ?? 0} + + + + {job.confidence_moderate ?? 0} + + + + {job.confidence_low ?? 0} + +
+
+ )}
{/* Right side - meta */} @@ -102,10 +142,9 @@ export function JobCard({ job }: JobCardProps) { {job.created_by}
- {job.total_lines && ( -
- - {job.total_lines} lines + {(job.total_output_rows ?? 0) > 0 && ( +
+ {job.total_output_rows} rows
)} {(job.total_estimated_cost ?? 0) > 0 && ( diff --git a/frontend/src/components/jobs/JobWizard/StepConfigure.tsx b/frontend/src/components/jobs/JobWizard/StepConfigure.tsx index fb4e616..860aa16 100644 --- a/frontend/src/components/jobs/JobWizard/StepConfigure.tsx +++ b/frontend/src/components/jobs/JobWizard/StepConfigure.tsx @@ -55,9 +55,22 @@ export function StepConfigure({ data, onChange, onNext }: StepConfigureProps) { useEffect(() => { getClients() - .then((data) => setClients(data)) + .then((fetched) => { + setClients(fetched); + // Auto-select Amazon if no client chosen yet + if (!data.client_id && fetched.length > 0) { + const amazon = fetched.find( + (c) => c.name.toLowerCase() === "amazon" + ); + const defaultClient = amazon || fetched[0]; + onChange({ + client_id: defaultClient.id, + client_name: defaultClient.name, + }); + } + }) .catch(() => {}); - }, []); + }, []); // eslint-disable-line react-hooks/exhaustive-deps const locales = data.job_type === "MAIN" ? MAIN_LOCALES : DERIVED_LOCALES; diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 24dd2ba..0afa5f0 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -115,6 +115,10 @@ export interface Job { total_token_usage?: number; total_estimated_cost?: number; error_message?: string; + confidence_high?: number; + confidence_moderate?: number; + confidence_low?: number; + total_output_rows?: number; } export interface LocaleInstance {