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:
@@ -1,9 +1,10 @@
|
||||
/**
|
||||
* Code Quality Dashboard Component
|
||||
* Manages the code quality dashboard page
|
||||
* Manages the unified code quality dashboard page
|
||||
* Supports multiple validator types: architecture, security, performance
|
||||
*/
|
||||
|
||||
// ✅ Use centralized logger
|
||||
// Use centralized logger
|
||||
const codeQualityLog = window.LogConfig.createLogger('CODE-QUALITY');
|
||||
|
||||
function codeQualityDashboard() {
|
||||
@@ -14,15 +15,23 @@ function codeQualityDashboard() {
|
||||
// Set current page for navigation
|
||||
currentPage: 'code-quality',
|
||||
|
||||
// Validator type selection
|
||||
selectedValidator: 'all', // 'all', 'architecture', 'security', 'performance'
|
||||
validatorTypes: ['architecture', 'security', 'performance'],
|
||||
|
||||
// Dashboard-specific data
|
||||
loading: false,
|
||||
scanning: false,
|
||||
error: null,
|
||||
successMessage: null,
|
||||
scanProgress: null, // Progress message during scan
|
||||
runningScans: [], // Track running scan IDs
|
||||
pollInterval: null, // Polling interval ID
|
||||
stats: {
|
||||
total_violations: 0,
|
||||
errors: 0,
|
||||
warnings: 0,
|
||||
info: 0,
|
||||
open: 0,
|
||||
assigned: 0,
|
||||
resolved: 0,
|
||||
@@ -33,11 +42,57 @@ function codeQualityDashboard() {
|
||||
by_rule: {},
|
||||
by_module: {},
|
||||
top_files: [],
|
||||
last_scan: null
|
||||
last_scan: null,
|
||||
validator_type: null,
|
||||
by_validator: {}
|
||||
},
|
||||
|
||||
async init() {
|
||||
// Check URL for validator_type parameter
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const urlValidator = urlParams.get('validator_type');
|
||||
if (urlValidator && this.validatorTypes.includes(urlValidator)) {
|
||||
this.selectedValidator = urlValidator;
|
||||
} else {
|
||||
// Ensure 'all' is explicitly set as default
|
||||
this.selectedValidator = 'all';
|
||||
}
|
||||
|
||||
await this.loadStats();
|
||||
|
||||
// Check for any running scans on page load
|
||||
await this.checkRunningScans();
|
||||
},
|
||||
|
||||
async checkRunningScans() {
|
||||
try {
|
||||
const runningScans = await apiClient.get('/admin/code-quality/scans/running');
|
||||
if (runningScans && runningScans.length > 0) {
|
||||
this.scanning = true;
|
||||
this.runningScans = runningScans.map(s => s.id);
|
||||
this.updateProgressMessage(runningScans);
|
||||
this.startPolling();
|
||||
}
|
||||
} catch (err) {
|
||||
codeQualityLog.error('Failed to check running scans:', err);
|
||||
}
|
||||
},
|
||||
|
||||
updateProgressMessage(scans) {
|
||||
const runningScans = scans.filter(s => s.status === 'running' || s.status === 'pending');
|
||||
if (runningScans.length === 0) {
|
||||
this.scanProgress = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Show progress from the first running scan
|
||||
const firstRunning = runningScans.find(s => s.status === 'running');
|
||||
if (firstRunning && firstRunning.progress_message) {
|
||||
this.scanProgress = firstRunning.progress_message;
|
||||
} else {
|
||||
const validatorNames = runningScans.map(s => this.capitalizeFirst(s.validator_type));
|
||||
this.scanProgress = `Running ${validatorNames.join(', ')} scan${runningScans.length > 1 ? 's' : ''}...`;
|
||||
}
|
||||
},
|
||||
|
||||
async loadStats() {
|
||||
@@ -45,7 +100,13 @@ function codeQualityDashboard() {
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const stats = await apiClient.get('/admin/code-quality/stats');
|
||||
// Build URL with validator_type filter if not 'all'
|
||||
let url = '/admin/code-quality/stats';
|
||||
if (this.selectedValidator !== 'all') {
|
||||
url += `?validator_type=${this.selectedValidator}`;
|
||||
}
|
||||
|
||||
const stats = await apiClient.get(url);
|
||||
this.stats = stats;
|
||||
} catch (err) {
|
||||
codeQualityLog.error('Failed to load stats:', err);
|
||||
@@ -60,37 +121,162 @@ function codeQualityDashboard() {
|
||||
}
|
||||
},
|
||||
|
||||
async runScan() {
|
||||
async selectValidator(validatorType) {
|
||||
if (this.selectedValidator !== validatorType) {
|
||||
this.selectedValidator = validatorType;
|
||||
await this.loadStats();
|
||||
|
||||
// Update URL without reload
|
||||
const url = new URL(window.location);
|
||||
if (validatorType === 'all') {
|
||||
url.searchParams.delete('validator_type');
|
||||
} else {
|
||||
url.searchParams.set('validator_type', validatorType);
|
||||
}
|
||||
window.history.pushState({}, '', url);
|
||||
}
|
||||
},
|
||||
|
||||
async runScan(validatorType = 'all') {
|
||||
this.scanning = true;
|
||||
this.error = null;
|
||||
this.successMessage = null;
|
||||
this.scanProgress = 'Queuing scan...';
|
||||
|
||||
try {
|
||||
const scan = await apiClient.post('/admin/code-quality/scan');
|
||||
this.successMessage = `Scan completed: ${scan.total_violations} violations found (${scan.errors} errors, ${scan.warnings} warnings)`;
|
||||
// Determine which validators to run
|
||||
const validatorTypesToRun = validatorType === 'all'
|
||||
? this.validatorTypes
|
||||
: [validatorType];
|
||||
|
||||
// Reload stats after scan
|
||||
await this.loadStats();
|
||||
const result = await apiClient.post('/admin/code-quality/scan', {
|
||||
validator_types: validatorTypesToRun
|
||||
});
|
||||
|
||||
// Store running scan IDs for polling
|
||||
this.runningScans = result.scans.map(s => s.id);
|
||||
|
||||
// Show initial status message
|
||||
const validatorNames = validatorTypesToRun.map(v => this.capitalizeFirst(v));
|
||||
this.scanProgress = `Running ${validatorNames.join(', ')} scan${validatorNames.length > 1 ? 's' : ''}...`;
|
||||
|
||||
// Start polling for completion
|
||||
this.startPolling();
|
||||
|
||||
// Clear success message after 5 seconds
|
||||
setTimeout(() => {
|
||||
this.successMessage = null;
|
||||
}, 5000);
|
||||
} catch (err) {
|
||||
codeQualityLog.error('Failed to run scan:', err);
|
||||
this.error = err.message;
|
||||
this.scanning = false;
|
||||
this.scanProgress = null;
|
||||
|
||||
// Redirect to login if unauthorized
|
||||
if (err.message.includes('Unauthorized')) {
|
||||
window.location.href = '/admin/login';
|
||||
}
|
||||
} finally {
|
||||
this.scanning = false;
|
||||
}
|
||||
},
|
||||
|
||||
startPolling() {
|
||||
// Clear any existing polling
|
||||
if (this.pollInterval) {
|
||||
clearInterval(this.pollInterval);
|
||||
}
|
||||
|
||||
// Poll every 3 seconds
|
||||
this.pollInterval = setInterval(async () => {
|
||||
await this.pollScanStatus();
|
||||
}, 3000);
|
||||
},
|
||||
|
||||
stopPolling() {
|
||||
if (this.pollInterval) {
|
||||
clearInterval(this.pollInterval);
|
||||
this.pollInterval = null;
|
||||
}
|
||||
},
|
||||
|
||||
async pollScanStatus() {
|
||||
if (this.runningScans.length === 0) {
|
||||
this.stopPolling();
|
||||
this.scanning = false;
|
||||
this.scanProgress = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const runningScans = await apiClient.get('/admin/code-quality/scans/running');
|
||||
|
||||
// Update progress message from running scans
|
||||
this.updateProgressMessage(runningScans);
|
||||
|
||||
// Check if our scans have completed
|
||||
const stillRunning = this.runningScans.filter(id =>
|
||||
runningScans.some(s => s.id === id)
|
||||
);
|
||||
|
||||
if (stillRunning.length === 0) {
|
||||
// All scans completed - get final results
|
||||
await this.handleScanCompletion();
|
||||
} else {
|
||||
// Update running scans list
|
||||
this.runningScans = stillRunning;
|
||||
}
|
||||
} catch (err) {
|
||||
codeQualityLog.error('Failed to poll scan status:', err);
|
||||
}
|
||||
},
|
||||
|
||||
async handleScanCompletion() {
|
||||
this.stopPolling();
|
||||
|
||||
// Get results for all completed scans
|
||||
let totalViolations = 0;
|
||||
let totalErrors = 0;
|
||||
let totalWarnings = 0;
|
||||
const completedScans = [];
|
||||
|
||||
for (const scanId of this.runningScans) {
|
||||
try {
|
||||
const scan = await apiClient.get(`/admin/code-quality/scans/${scanId}/status`);
|
||||
completedScans.push(scan);
|
||||
totalViolations += scan.total_violations || 0;
|
||||
totalErrors += scan.errors || 0;
|
||||
totalWarnings += scan.warnings || 0;
|
||||
} catch (err) {
|
||||
codeQualityLog.error(`Failed to get scan ${scanId} results:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
// Format success message based on number of validators run
|
||||
if (completedScans.length > 1) {
|
||||
this.successMessage = `Scan completed: ${totalViolations} total violations found (${totalErrors} errors, ${totalWarnings} warnings) across ${completedScans.length} validators`;
|
||||
} else if (completedScans.length === 1) {
|
||||
const scan = completedScans[0];
|
||||
this.successMessage = `${this.capitalizeFirst(scan.validator_type)} scan completed: ${scan.total_violations} violations found (${scan.errors} errors, ${scan.warnings} warnings)`;
|
||||
} else {
|
||||
this.successMessage = 'Scan completed';
|
||||
}
|
||||
|
||||
// Reload stats after scan
|
||||
await this.loadStats();
|
||||
|
||||
// Reset scanning state
|
||||
this.scanning = false;
|
||||
this.scanProgress = null;
|
||||
this.runningScans = [];
|
||||
|
||||
// Clear success message after 5 seconds
|
||||
setTimeout(() => {
|
||||
this.successMessage = null;
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
async refresh() {
|
||||
await this.loadStats();
|
||||
},
|
||||
|
||||
capitalizeFirst(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user