feat: implement code quality dashboard with architecture violation tracking

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>
This commit is contained in:
2025-11-28 09:40:14 +01:00
parent 74bf2367f8
commit 9db0da25ec
11 changed files with 1854 additions and 4 deletions

View File

@@ -0,0 +1,448 @@
"""
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)