feat: implement background task architecture for code quality scans

- Add status fields to ArchitectureScan model (status, started_at,
  completed_at, error_message, progress_message)
- Create database migration for new status fields
- Create background task function execute_code_quality_scan()
- Update API to return 202 with job IDs and support polling
- Add code quality scans to unified BackgroundTasksService
- Integrate scans into background tasks API and page
- Implement frontend polling with 3-second interval
- Add progress banner showing scan status
- Users can navigate away while scans run in background
- Document the implementation in architecture docs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-21 20:57:08 +01:00
parent 9cf0a568c0
commit 6a903e16c6
10 changed files with 1710 additions and 83 deletions

View File

@@ -22,7 +22,7 @@ class BackgroundTaskResponse(BaseModel):
"""Unified background task response"""
id: int
task_type: str # 'import' or 'test_run'
task_type: str # 'import', 'test_run', or 'code_quality_scan'
status: str
started_at: str | None
completed_at: str | None
@@ -46,6 +46,7 @@ class BackgroundTasksStatsResponse(BaseModel):
# By type
import_jobs: dict
test_runs: dict
code_quality_scans: dict
def _convert_import_to_response(job) -> BackgroundTaskResponse:
@@ -107,11 +108,47 @@ def _convert_test_run_to_response(run) -> BackgroundTaskResponse:
)
def _convert_scan_to_response(scan) -> BackgroundTaskResponse:
"""Convert ArchitectureScan to BackgroundTaskResponse"""
duration = scan.duration_seconds
if scan.status in ["pending", "running"] and scan.started_at:
duration = (datetime.now(UTC) - scan.started_at).total_seconds()
# Map validator type to human-readable name
validator_names = {
"architecture": "Architecture",
"security": "Security",
"performance": "Performance",
}
validator_name = validator_names.get(scan.validator_type, scan.validator_type)
return BackgroundTaskResponse(
id=scan.id,
task_type="code_quality_scan",
status=scan.status,
started_at=scan.started_at.isoformat() if scan.started_at else None,
completed_at=scan.completed_at.isoformat() if scan.completed_at else None,
duration_seconds=duration,
description=f"{validator_name} code quality scan",
triggered_by=scan.triggered_by,
error_message=scan.error_message,
details={
"validator_type": scan.validator_type,
"total_files": scan.total_files,
"total_violations": scan.total_violations,
"errors": scan.errors,
"warnings": scan.warnings,
"git_commit_hash": scan.git_commit_hash,
"progress_message": scan.progress_message,
},
)
@router.get("/tasks", response_model=list[BackgroundTaskResponse])
async def list_background_tasks(
status: str | None = Query(None, description="Filter by status"),
task_type: str | None = Query(
None, description="Filter by type (import, test_run)"
None, description="Filter by type (import, test_run, code_quality_scan)"
),
limit: int = Query(50, ge=1, le=200),
db: Session = Depends(get_db),
@@ -120,7 +157,7 @@ async def list_background_tasks(
"""
List all background tasks across the system
Returns a unified view of import jobs and test runs.
Returns a unified view of import jobs, test runs, and code quality scans.
"""
tasks = []
@@ -138,6 +175,13 @@ async def list_background_tasks(
)
tasks.extend([_convert_test_run_to_response(run) for run in test_runs])
# Get code quality scans
if task_type is None or task_type == "code_quality_scan":
scans = background_tasks_service.get_code_quality_scans(
db, status=status, limit=limit
)
tasks.extend([_convert_scan_to_response(scan) for scan in scans])
# Sort by start time (most recent first)
tasks.sort(
key=lambda t: t.started_at or "1970-01-01T00:00:00",
@@ -157,22 +201,31 @@ async def get_background_tasks_stats(
"""
import_stats = background_tasks_service.get_import_stats(db)
test_stats = background_tasks_service.get_test_run_stats(db)
scan_stats = background_tasks_service.get_scan_stats(db)
# Combined stats
total_running = import_stats["running"] + test_stats["running"]
total_completed = import_stats["completed"] + test_stats["completed"]
total_failed = import_stats["failed"] + test_stats["failed"]
total_tasks = import_stats["total"] + test_stats["total"]
total_running = (
import_stats["running"] + test_stats["running"] + scan_stats["running"]
)
total_completed = (
import_stats["completed"] + test_stats["completed"] + scan_stats["completed"]
)
total_failed = (
import_stats["failed"] + test_stats["failed"] + scan_stats["failed"]
)
total_tasks = import_stats["total"] + test_stats["total"] + scan_stats["total"]
tasks_today = import_stats["today"] + test_stats["today"] + scan_stats["today"]
return BackgroundTasksStatsResponse(
total_tasks=total_tasks,
running=total_running,
completed=total_completed,
failed=total_failed,
tasks_today=import_stats["today"] + test_stats["today"],
tasks_today=tasks_today,
avg_duration_seconds=test_stats.get("avg_duration"),
import_jobs=import_stats,
test_runs=test_stats,
code_quality_scans=scan_stats,
)
@@ -194,4 +247,8 @@ async def list_running_tasks(
running_tests = background_tasks_service.get_running_test_runs(db)
tasks.extend([_convert_test_run_to_response(run) for run in running_tests])
# Running code quality scans
running_scans = background_tasks_service.get_running_scans(db)
tasks.extend([_convert_scan_to_response(scan) for scan in running_scans])
return tasks