Implement comprehensive code quality dashboard (Phase 2-4) to track and manage
architecture violations found by the validation script.
Backend Implementation:
- Add JSON output support to validate_architecture.py script
- Create CodeQualityService with scan management, violation tracking, and statistics
- Implement REST API endpoints for code quality management:
* POST /admin/code-quality/scan - trigger new architecture scan
* GET /admin/code-quality/scans - list scan history
* GET /admin/code-quality/violations - list violations with filtering/pagination
* GET /admin/code-quality/violations/{id} - get violation details
* POST /admin/code-quality/violations/{id}/assign - assign to developer
* POST /admin/code-quality/violations/{id}/resolve - mark as resolved
* POST /admin/code-quality/violations/{id}/ignore - mark as ignored
* POST /admin/code-quality/violations/{id}/comments - add comments
* GET /admin/code-quality/stats - dashboard statistics
- Fix architecture_scan model imports to use app.core.database
Frontend Implementation:
- Create code quality dashboard page (code-quality-dashboard.html)
* Summary cards for total violations, errors, warnings, health score
* Status breakdown (open, assigned, resolved, ignored)
* Trend visualization for last 7 scans
* Top violating files list
* Violations by rule and module
* Quick action links
- Create violations list page (code-quality-violations.html)
* Filterable table by severity, status, rule ID, file path
* Pagination support
* Violation detail view links
- Add Alpine.js components (code-quality-dashboard.js, code-quality-violations.js)
* Dashboard state management and scan triggering
* Violations list with filtering and pagination
* API integration with authentication
- Add "Code Quality" navigation link in admin sidebar (Developer Tools section)
Routes:
- GET /admin/code-quality - dashboard page
- GET /admin/code-quality/violations - violations list
- GET /admin/code-quality/violations/{id} - violation details
Features:
- Real-time scan execution from UI
- Technical debt score calculation (0-100 scale)
- Violation workflow: open → assigned → resolved/ignored
- Trend tracking across multiple scans
- File and module-level insights
- Assignment system with priorities and due dates
- Collaborative comments on violations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
449 lines
13 KiB
Python
449 lines
13 KiB
Python
"""
|
|
Code Quality API Endpoints
|
|
RESTful API for architecture validation and violation management
|
|
"""
|
|
|
|
from typing import Optional
|
|
from datetime import datetime
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy.orm import Session
|
|
from pydantic import BaseModel, Field
|
|
|
|
from app.core.database import get_db
|
|
from app.services.code_quality_service import code_quality_service
|
|
from app.api.deps import get_current_admin_api
|
|
from models.database.user import User
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
# Pydantic Models for API
|
|
|
|
class ScanResponse(BaseModel):
|
|
"""Response model for a scan"""
|
|
id: int
|
|
timestamp: str
|
|
total_files: int
|
|
total_violations: int
|
|
errors: int
|
|
warnings: int
|
|
duration_seconds: float
|
|
triggered_by: str
|
|
git_commit_hash: Optional[str]
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class ViolationResponse(BaseModel):
|
|
"""Response model for a violation"""
|
|
id: int
|
|
scan_id: int
|
|
rule_id: str
|
|
rule_name: str
|
|
severity: str
|
|
file_path: str
|
|
line_number: int
|
|
message: str
|
|
context: Optional[str]
|
|
suggestion: Optional[str]
|
|
status: str
|
|
assigned_to: Optional[int]
|
|
resolved_at: Optional[str]
|
|
resolved_by: Optional[int]
|
|
resolution_note: Optional[str]
|
|
created_at: str
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class ViolationListResponse(BaseModel):
|
|
"""Response model for paginated violations list"""
|
|
violations: list[ViolationResponse]
|
|
total: int
|
|
page: int
|
|
page_size: int
|
|
total_pages: int
|
|
|
|
|
|
class ViolationDetailResponse(ViolationResponse):
|
|
"""Response model for single violation with relationships"""
|
|
assignments: list = []
|
|
comments: list = []
|
|
|
|
|
|
class AssignViolationRequest(BaseModel):
|
|
"""Request model for assigning a violation"""
|
|
user_id: int = Field(..., description="User ID to assign to")
|
|
due_date: Optional[datetime] = Field(None, description="Due date for resolution")
|
|
priority: str = Field("medium", description="Priority level (low, medium, high, critical)")
|
|
|
|
|
|
class ResolveViolationRequest(BaseModel):
|
|
"""Request model for resolving a violation"""
|
|
resolution_note: str = Field(..., description="Note about the resolution")
|
|
|
|
|
|
class IgnoreViolationRequest(BaseModel):
|
|
"""Request model for ignoring a violation"""
|
|
reason: str = Field(..., description="Reason for ignoring")
|
|
|
|
|
|
class AddCommentRequest(BaseModel):
|
|
"""Request model for adding a comment"""
|
|
comment: str = Field(..., min_length=1, description="Comment text")
|
|
|
|
|
|
class DashboardStatsResponse(BaseModel):
|
|
"""Response model for dashboard statistics"""
|
|
total_violations: int
|
|
errors: int
|
|
warnings: int
|
|
open: int
|
|
assigned: int
|
|
resolved: int
|
|
ignored: int
|
|
technical_debt_score: int
|
|
trend: list
|
|
by_severity: dict
|
|
by_rule: dict
|
|
by_module: dict
|
|
top_files: list
|
|
last_scan: Optional[str]
|
|
|
|
|
|
# API Endpoints
|
|
|
|
@router.post("/scan", response_model=ScanResponse)
|
|
async def trigger_scan(
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_admin_api)
|
|
):
|
|
"""
|
|
Trigger a new architecture scan
|
|
|
|
Requires authentication. Runs the validator script and stores results.
|
|
"""
|
|
try:
|
|
scan = code_quality_service.run_scan(db, triggered_by=f"manual:{current_user.username}")
|
|
|
|
return ScanResponse(
|
|
id=scan.id,
|
|
timestamp=scan.timestamp.isoformat(),
|
|
total_files=scan.total_files,
|
|
total_violations=scan.total_violations,
|
|
errors=scan.errors,
|
|
warnings=scan.warnings,
|
|
duration_seconds=scan.duration_seconds,
|
|
triggered_by=scan.triggered_by,
|
|
git_commit_hash=scan.git_commit_hash
|
|
)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Scan failed: {str(e)}")
|
|
|
|
|
|
@router.get("/scans", response_model=list[ScanResponse])
|
|
async def list_scans(
|
|
limit: int = Query(30, ge=1, le=100, description="Number of scans to return"),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_admin_api)
|
|
):
|
|
"""
|
|
Get scan history
|
|
|
|
Returns recent scans for trend analysis.
|
|
"""
|
|
scans = code_quality_service.get_scan_history(db, limit=limit)
|
|
|
|
return [
|
|
ScanResponse(
|
|
id=scan.id,
|
|
timestamp=scan.timestamp.isoformat(),
|
|
total_files=scan.total_files,
|
|
total_violations=scan.total_violations,
|
|
errors=scan.errors,
|
|
warnings=scan.warnings,
|
|
duration_seconds=scan.duration_seconds,
|
|
triggered_by=scan.triggered_by,
|
|
git_commit_hash=scan.git_commit_hash
|
|
)
|
|
for scan in scans
|
|
]
|
|
|
|
|
|
@router.get("/violations", response_model=ViolationListResponse)
|
|
async def list_violations(
|
|
scan_id: Optional[int] = Query(None, description="Filter by scan ID (defaults to latest)"),
|
|
severity: Optional[str] = Query(None, description="Filter by severity (error, warning)"),
|
|
status: Optional[str] = Query(None, description="Filter by status (open, assigned, resolved, ignored)"),
|
|
rule_id: Optional[str] = Query(None, description="Filter by rule ID"),
|
|
file_path: Optional[str] = Query(None, description="Filter by file path (partial match)"),
|
|
page: int = Query(1, ge=1, description="Page number"),
|
|
page_size: int = Query(50, ge=1, le=200, description="Items per page"),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_admin_api)
|
|
):
|
|
"""
|
|
Get violations with filtering and pagination
|
|
|
|
Returns violations from latest scan by default.
|
|
"""
|
|
offset = (page - 1) * page_size
|
|
|
|
violations, total = code_quality_service.get_violations(
|
|
db,
|
|
scan_id=scan_id,
|
|
severity=severity,
|
|
status=status,
|
|
rule_id=rule_id,
|
|
file_path=file_path,
|
|
limit=page_size,
|
|
offset=offset
|
|
)
|
|
|
|
total_pages = (total + page_size - 1) // page_size
|
|
|
|
return ViolationListResponse(
|
|
violations=[
|
|
ViolationResponse(
|
|
id=v.id,
|
|
scan_id=v.scan_id,
|
|
rule_id=v.rule_id,
|
|
rule_name=v.rule_name,
|
|
severity=v.severity,
|
|
file_path=v.file_path,
|
|
line_number=v.line_number,
|
|
message=v.message,
|
|
context=v.context,
|
|
suggestion=v.suggestion,
|
|
status=v.status,
|
|
assigned_to=v.assigned_to,
|
|
resolved_at=v.resolved_at.isoformat() if v.resolved_at else None,
|
|
resolved_by=v.resolved_by,
|
|
resolution_note=v.resolution_note,
|
|
created_at=v.created_at.isoformat()
|
|
)
|
|
for v in violations
|
|
],
|
|
total=total,
|
|
page=page,
|
|
page_size=page_size,
|
|
total_pages=total_pages
|
|
)
|
|
|
|
|
|
@router.get("/violations/{violation_id}", response_model=ViolationDetailResponse)
|
|
async def get_violation(
|
|
violation_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_admin_api)
|
|
):
|
|
"""
|
|
Get single violation with details
|
|
|
|
Includes assignments and comments.
|
|
"""
|
|
violation = code_quality_service.get_violation_by_id(db, violation_id)
|
|
|
|
if not violation:
|
|
raise HTTPException(status_code=404, detail="Violation not found")
|
|
|
|
# Format assignments
|
|
assignments = [
|
|
{
|
|
'id': a.id,
|
|
'user_id': a.user_id,
|
|
'assigned_at': a.assigned_at.isoformat(),
|
|
'assigned_by': a.assigned_by,
|
|
'due_date': a.due_date.isoformat() if a.due_date else None,
|
|
'priority': a.priority
|
|
}
|
|
for a in violation.assignments
|
|
]
|
|
|
|
# Format comments
|
|
comments = [
|
|
{
|
|
'id': c.id,
|
|
'user_id': c.user_id,
|
|
'comment': c.comment,
|
|
'created_at': c.created_at.isoformat()
|
|
}
|
|
for c in violation.comments
|
|
]
|
|
|
|
return ViolationDetailResponse(
|
|
id=violation.id,
|
|
scan_id=violation.scan_id,
|
|
rule_id=violation.rule_id,
|
|
rule_name=violation.rule_name,
|
|
severity=violation.severity,
|
|
file_path=violation.file_path,
|
|
line_number=violation.line_number,
|
|
message=violation.message,
|
|
context=violation.context,
|
|
suggestion=violation.suggestion,
|
|
status=violation.status,
|
|
assigned_to=violation.assigned_to,
|
|
resolved_at=violation.resolved_at.isoformat() if violation.resolved_at else None,
|
|
resolved_by=violation.resolved_by,
|
|
resolution_note=violation.resolution_note,
|
|
created_at=violation.created_at.isoformat(),
|
|
assignments=assignments,
|
|
comments=comments
|
|
)
|
|
|
|
|
|
@router.post("/violations/{violation_id}/assign")
|
|
async def assign_violation(
|
|
violation_id: int,
|
|
request: AssignViolationRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_admin_api)
|
|
):
|
|
"""
|
|
Assign violation to a developer
|
|
|
|
Updates violation status to 'assigned'.
|
|
"""
|
|
try:
|
|
assignment = code_quality_service.assign_violation(
|
|
db,
|
|
violation_id=violation_id,
|
|
user_id=request.user_id,
|
|
assigned_by=current_user.id,
|
|
due_date=request.due_date,
|
|
priority=request.priority
|
|
)
|
|
|
|
return {
|
|
'id': assignment.id,
|
|
'violation_id': assignment.violation_id,
|
|
'user_id': assignment.user_id,
|
|
'assigned_at': assignment.assigned_at.isoformat(),
|
|
'assigned_by': assignment.assigned_by,
|
|
'due_date': assignment.due_date.isoformat() if assignment.due_date else None,
|
|
'priority': assignment.priority
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
|
@router.post("/violations/{violation_id}/resolve")
|
|
async def resolve_violation(
|
|
violation_id: int,
|
|
request: ResolveViolationRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_admin_api)
|
|
):
|
|
"""
|
|
Mark violation as resolved
|
|
|
|
Records resolution timestamp and user.
|
|
"""
|
|
try:
|
|
violation = code_quality_service.resolve_violation(
|
|
db,
|
|
violation_id=violation_id,
|
|
resolved_by=current_user.id,
|
|
resolution_note=request.resolution_note
|
|
)
|
|
|
|
return {
|
|
'id': violation.id,
|
|
'status': violation.status,
|
|
'resolved_at': violation.resolved_at.isoformat() if violation.resolved_at else None,
|
|
'resolved_by': violation.resolved_by,
|
|
'resolution_note': violation.resolution_note
|
|
}
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
|
@router.post("/violations/{violation_id}/ignore")
|
|
async def ignore_violation(
|
|
violation_id: int,
|
|
request: IgnoreViolationRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_admin_api)
|
|
):
|
|
"""
|
|
Mark violation as ignored (won't fix)
|
|
|
|
Records reason for ignoring.
|
|
"""
|
|
try:
|
|
violation = code_quality_service.ignore_violation(
|
|
db,
|
|
violation_id=violation_id,
|
|
ignored_by=current_user.id,
|
|
reason=request.reason
|
|
)
|
|
|
|
return {
|
|
'id': violation.id,
|
|
'status': violation.status,
|
|
'resolved_at': violation.resolved_at.isoformat() if violation.resolved_at else None,
|
|
'resolved_by': violation.resolved_by,
|
|
'resolution_note': violation.resolution_note
|
|
}
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
|
@router.post("/violations/{violation_id}/comments")
|
|
async def add_comment(
|
|
violation_id: int,
|
|
request: AddCommentRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_admin_api)
|
|
):
|
|
"""
|
|
Add comment to violation
|
|
|
|
For team collaboration and discussion.
|
|
"""
|
|
try:
|
|
comment = code_quality_service.add_comment(
|
|
db,
|
|
violation_id=violation_id,
|
|
user_id=current_user.id,
|
|
comment=request.comment
|
|
)
|
|
|
|
return {
|
|
'id': comment.id,
|
|
'violation_id': comment.violation_id,
|
|
'user_id': comment.user_id,
|
|
'comment': comment.comment,
|
|
'created_at': comment.created_at.isoformat()
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
|
@router.get("/stats", response_model=DashboardStatsResponse)
|
|
async def get_dashboard_stats(
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_admin_api)
|
|
):
|
|
"""
|
|
Get dashboard statistics
|
|
|
|
Returns comprehensive stats for the dashboard including:
|
|
- Total counts by severity and status
|
|
- Technical debt score
|
|
- Trend data (last 7 scans)
|
|
- Top violating files
|
|
- Violations by rule and module
|
|
"""
|
|
stats = code_quality_service.get_dashboard_stats(db)
|
|
|
|
return DashboardStatsResponse(**stats)
|