Files
orion/static/admin/js/code-quality-dashboard.js
Samir Boulahtit 6a903e16c6 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>
2025-12-21 20:57:08 +01:00

283 lines
9.9 KiB
JavaScript

/**
* Code Quality Dashboard Component
* Manages the unified code quality dashboard page
* Supports multiple validator types: architecture, security, performance
*/
// Use centralized logger
const codeQualityLog = window.LogConfig.createLogger('CODE-QUALITY');
function codeQualityDashboard() {
return {
// Extend base data
...data(),
// 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,
ignored: 0,
technical_debt_score: 100,
trend: [],
by_severity: {},
by_rule: {},
by_module: {},
top_files: [],
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() {
this.loading = true;
this.error = null;
try {
// 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);
this.error = err.message;
// Redirect to login if unauthorized
if (err.message.includes('Unauthorized')) {
window.location.href = '/admin/login';
}
} finally {
this.loading = false;
}
},
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 {
// Determine which validators to run
const validatorTypesToRun = validatorType === 'all'
? this.validatorTypes
: [validatorType];
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();
} 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';
}
}
},
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);
}
};
}