netgescon-day0/resources/views/admin/code-quality/index.blade.php

701 lines
29 KiB
PHP
Executable File

@extends('admin.layouts.netgescon')
@section('page-title', 'Controllo Qualità Codice NetGescon')
@section('content')
<div class="space-y-8 netgescon-fade-in">
<!-- Header -->
<div class="netgescon-card">
<div class="flex items-center justify-between">
<div>
<h2 class="netgescon-title">
<i class="fas fa-code text-green-500 mr-3"></i>
Controllo Qualità Codice NetGescon
</h2>
<p class="netgescon-text mt-2">
Sistema automatico di controllo standard di codifica e componenti NetGescon
</p>
</div>
<div class="hidden md:block">
<button onclick="startScan()" class="netgescon-btn netgescon-btn-primary">
<i class="fas fa-search mr-2"></i>
Avvia Scansione
</button>
</div>
</div>
</div>
<!-- Filtri di Scansione -->
<div class="netgescon-card">
<h3 class="netgescon-subtitle mb-4">
<i class="fas fa-filter mr-2"></i>
Configurazione Scansione
</h3>
<form id="scanForm" class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<label class="netgescon-label">Percorsi da Scansionare</label>
<input type="text" name="path" value="{{ $scanPath ?? 'app,resources/views' }}"
class="netgescon-input" placeholder="app,resources/views,routes">
<p class="netgescon-text-small mt-1">Separa più percorsi con virgole</p>
<div class="flex flex-wrap gap-2 mt-2">
<button type="button" class="netgescon-badge netgescon-badge-secondary" onclick="appendPath('app')">app</button>
<button type="button" class="netgescon-badge netgescon-badge-secondary" onclick="appendPath('resources/views')">resources/views</button>
<button type="button" class="netgescon-badge netgescon-badge-secondary" onclick="appendPath('routes')">routes</button>
<button type="button" class="netgescon-badge netgescon-badge-secondary" onclick="appendPath('database/migrations')">database/migrations</button>
</div>
</div>
<div>
<label class="netgescon-label">Filtro Severità</label>
<select name="severity" class="netgescon-input">
<option value="all" {{ ($severityFilter ?? 'all') === 'all' ? 'selected' : '' }}>Tutti</option>
<option value="critical" {{ ($severityFilter ?? '') === 'critical' ? 'selected' : '' }}>🔴 Critical</option>
<option value="error" {{ ($severityFilter ?? '') === 'error' ? 'selected' : '' }}>🟠 Error</option>
<option value="warning" {{ ($severityFilter ?? '') === 'warning' ? 'selected' : '' }}>🟡 Warning</option>
<option value="info" {{ ($severityFilter ?? '') === 'info' ? 'selected' : '' }}>🔵 Info</option>
</select>
</div>
<div>
<label class="netgescon-label">Modulo</label>
<select name="module" class="netgescon-input">
<option value="all" {{ ($moduleFilter ?? 'all') === 'all' ? 'selected' : '' }}>Tutti</option>
<option value="gescon-import" {{ ($moduleFilter ?? '') === 'gescon-import' ? 'selected' : '' }}>Gescon Import</option>
<option value="stabili" {{ ($moduleFilter ?? '') === 'stabili' ? 'selected' : '' }}>Stabili</option>
<option value="contabilita-gescon" {{ ($moduleFilter ?? '') === 'contabilita-gescon' ? 'selected' : '' }}>Contabilità Gescon</option>
<option value="supporto" {{ ($moduleFilter ?? '') === 'supporto' ? 'selected' : '' }}>Supporto</option>
</select>
</div>
<div>
<label class="netgescon-label">Menù</label>
<select name="menu" class="netgescon-input">
<option value="all" {{ ($menuFilter ?? 'all') === 'all' ? 'selected' : '' }}>Tutti</option>
<option value="stabili" {{ ($menuFilter ?? '') === 'stabili' ? 'selected' : '' }}>Stabili</option>
<option value="condomini" {{ ($menuFilter ?? '') === 'condomini' ? 'selected' : '' }}>Condomini</option>
<option value="amministrazione" {{ ($menuFilter ?? '') === 'amministrazione' ? 'selected' : '' }}>Amministrazione</option>
<option value="contabilita" {{ ($menuFilter ?? '') === 'contabilita' ? 'selected' : '' }}>Contabilità</option>
<option value="gestione-economica" {{ ($menuFilter ?? '') === 'gestione-economica' ? 'selected' : '' }}>Gestione Economica</option>
<option value="moduli" {{ ($menuFilter ?? '') === 'moduli' ? 'selected' : '' }}>Moduli</option>
<option value="gescon-import" {{ ($menuFilter ?? '') === 'gescon-import' ? 'selected' : '' }}>Gescon Import</option>
<option value="supporto" {{ ($menuFilter ?? '') === 'supporto' ? 'selected' : '' }}>Supporto</option>
</select>
</div>
<div class="flex items-end">
<button type="button" onclick="startScan()" class="netgescon-btn netgescon-btn-secondary w-full">
<i class="fas fa-play mr-2"></i>
Avvia Scansione
</button>
</div>
<div class="md:col-span-3">
<div class="rounded-md border border-blue-200 bg-blue-50 px-3 py-2">
<input type="hidden" name="include_data_checks" value="0">
<label class="inline-flex items-center gap-2 text-sm text-blue-900">
<input type="checkbox" name="include_data_checks" value="1" {{ ($includeDataChecks ?? true) ? 'checked' : '' }}>
<span>Controlli integrità archivi (millesimali, legacy individuali, correlazioni DB)</span>
</label>
</div>
</div>
</form>
<div class="mt-4 flex flex-wrap gap-2">
<button type="button" class="netgescon-btn netgescon-btn-primary" onclick="quickScanGesconImport()">
<i class="fas fa-bolt mr-2"></i> Scansione Rapida: Gescon Import
</button>
<button type="button" class="netgescon-btn" onclick="exportResults('csv')">
<i class="fas fa-file-csv mr-2"></i> Esporta CSV
</button>
<button type="button" class="netgescon-btn" onclick="exportResults('json')">
<i class="fas fa-file-code mr-2"></i> Esporta JSON
</button>
</div>
</div>
<!-- Strumenti Rapidi (Lint & Editor) -->
<div class="netgescon-card">
<h3 class="netgescon-subtitle mb-4 flex items-center gap-2">
<i class="fas fa-tools text-gray-500"></i>
Strumenti Rapidi Qualità
</h3>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="md:col-span-3">
<label class="netgescon-label">File relativo (app/, resources/views/, routes/)</label>
<input type="text" id="quickToolsFile" class="netgescon-input" placeholder="es. resources/views/admin/gescon-import/tabs/mapping.blade.php">
<p class="netgescon-text-small mt-1">Percorso relativo dalla root del progetto. Usa la scansione per copiare rapidamente il path.</p>
</div>
<div class="flex items-end gap-2">
<button type="button" class="netgescon-btn netgescon-btn-secondary flex-1" onclick="lintFileFromInput()">
<i class="fas fa-stethoscope mr-2"></i>Controlla Sintassi
</button>
<button type="button" class="netgescon-btn netgescon-btn-outline-primary flex-1" onclick="openQuickEditFromInput()">
<i class="fas fa-edit mr-2"></i>Editor Rapido
</button>
</div>
</div>
<pre id="quickToolsOutput" class="mt-4 bg-gray-900 text-green-200 text-xs p-3 rounded hidden overflow-x-auto"></pre>
</div>
<!-- Loading -->
<div id="scanLoading" class="netgescon-card hidden">
<div class="text-center py-8">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
<p class="netgescon-text">Scansione in corso...</p>
</div>
</div>
<!-- Risultati Summary -->
<div id="scanSummary" class="hidden">
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="netgescon-card">
<div class="flex items-center">
<div class="p-3 rounded-full bg-blue-100">
<i class="fas fa-file-code text-blue-600 text-xl"></i>
</div>
<div class="ml-4">
<p class="netgescon-text-small">File Analizzati</p>
<p class="netgescon-number" id="totalFiles">{{ $totalScanned ?? 0 }}</p>
</div>
</div>
</div>
<div class="netgescon-card">
<div class="flex items-center">
<div class="p-3 rounded-full bg-red-100">
<i class="fas fa-exclamation-triangle text-red-600 text-xl"></i>
</div>
<div class="ml-4">
<p class="netgescon-text-small">Problemi Totali</p>
<p class="netgescon-number" id="totalIssues">0</p>
</div>
</div>
</div>
<div class="netgescon-card">
<div class="flex items-center">
<div class="p-3 rounded-full bg-red-100">
<i class="fas fa-ban text-red-600 text-xl"></i>
</div>
<div class="ml-4">
<p class="netgescon-text-small">Critical</p>
<p class="netgescon-number text-red-600" id="criticalCount">0</p>
</div>
</div>
</div>
<div class="netgescon-card">
<div class="flex items-center">
<div class="p-3 rounded-full bg-orange-100">
<i class="fas fa-times-circle text-orange-600 text-xl"></i>
</div>
<div class="ml-4">
<p class="netgescon-text-small">Errori</p>
<p class="netgescon-number text-orange-600" id="errorCount">0</p>
</div>
</div>
</div>
</div>
<div id="ruleSummary" class="netgescon-card"></div>
<div id="moduleMenuSummary" class="netgescon-card mt-4"></div>
</div>
<!-- Risultati Dettagliati -->
<div id="scanResults" class="space-y-6 hidden">
<!-- I risultati saranno popolati dinamicamente -->
</div>
</div>
<script>
// Modal semplice per mostrare il contenuto proposto dall'Autofix
let autofixModal;
function ensureAutofixModal() {
if (autofixModal) return autofixModal;
const modal = document.createElement('div');
modal.id = 'autofixModal';
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden';
modal.innerHTML = `
<div class="bg-white rounded-lg shadow-xl w-11/12 md:w-3/4 lg:w-1/2 max-h-[80vh] overflow-hidden">
<div class="px-6 py-4 border-b flex items-center justify-between">
<h3 class="font-semibold text-gray-800">Anteprima Fix Automatico</h3>
<button class="text-gray-500 hover:text-gray-700" onclick="closeAutofixModal()">✕</button>
</div>
<div class="p-4 space-y-3">
<div class="text-sm text-gray-600" id="autofixFile"></div>
<textarea id="autofixContent" class="w-full h-64 border rounded p-2 font-mono text-sm"></textarea>
<div id="autofixHints" class="text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded p-2 hidden"></div>
</div>
<div class="px-6 py-3 bg-gray-50 border-t flex items-center justify-end gap-2">
<button class="netgescon-btn" onclick="copyAutofixContent()"><i class="fas fa-copy mr-1"></i>Copia</button>
<button class="netgescon-btn netgescon-btn-primary hidden" id="autofixSaveBtn" onclick="saveQuickEdit()"><i class="fas fa-save mr-1"></i>Salva</button>
<button class="netgescon-btn netgescon-btn-secondary" onclick="closeAutofixModal()">Chiudi</button>
</div>
</div>`;
document.body.appendChild(modal);
autofixModal = modal;
return modal;
}
function openAutofixModal(file, content, hints = [], mode = 'autofix') {
const modal = ensureAutofixModal();
modal.dataset.mode = mode;
modal.dataset.file = file || '';
const fileLabel = modal.querySelector('#autofixFile');
fileLabel.textContent = 'File: ' + (file || '');
fileLabel.dataset.file = file || '';
const textarea = modal.querySelector('#autofixContent');
textarea.value = content || '';
textarea.readOnly = mode !== 'edit';
const saveBtn = modal.querySelector('#autofixSaveBtn');
if (mode === 'edit') {
saveBtn.classList.remove('hidden');
} else {
saveBtn.classList.add('hidden');
}
const hintsBox = modal.querySelector('#autofixHints');
if (hints && hints.length) {
hintsBox.innerHTML = '<ul class="list-disc pl-5">' + hints.map(h => `<li>${h}</li>`).join('') + '</ul>';
hintsBox.classList.remove('hidden');
} else {
hintsBox.classList.add('hidden');
hintsBox.innerHTML = '';
}
modal.classList.remove('hidden');
}
function closeAutofixModal() { if (autofixModal) autofixModal.classList.add('hidden'); }
function copyAutofixContent() {
const ta = document.getElementById('autofixContent');
ta.select();
document.execCommand('copy');
}
function saveQuickEdit() {
const modal = ensureAutofixModal();
const file = modal.dataset.file || modal.querySelector('#autofixFile')?.dataset.file;
if (!file) {
alert('Nessun file selezionato.');
return;
}
const content = modal.querySelector('#autofixContent').value;
fetch('{{ route('admin.code-quality.file') }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
},
body: JSON.stringify({ file, action: 'save', content })
})
.then(res => res.json())
.then(data => {
if (!data.success) {
alert(data.message || 'Errore salvataggio file');
return;
}
showQualityOutput('File salvato: ' + file, 'success');
closeAutofixModal();
})
.catch(err => {
console.error(err);
alert('Errore salvataggio: ' + err);
});
}
function lintFile(file, options = {}) {
if (!file) {
alert('Specificare un file da controllare');
return;
}
fetch('{{ route('admin.code-quality.lint') }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
},
body: JSON.stringify({ file })
})
.then(res => res.json())
.then(data => {
const output = (data.output || []).join('\n');
const baseMsg = data.success ? '✅ Sintassi ok' : '⚠️ Sintassi da correggere';
const message = baseMsg + (output ? '\n' + output : '');
if (options.target) {
showQualityOutput(message, data.success ? 'success' : 'error', options.target);
} else {
alert(message);
}
})
.catch(error => {
console.error(error);
if (options.target) {
showQualityOutput('Errore lint: ' + error, 'error', options.target);
} else {
alert('Errore durante il lint: ' + error);
}
});
}
function lintFileFromInput() {
const file = document.getElementById('quickToolsFile').value.trim();
if (!file) {
alert('Inserisci un percorso file relativo.');
return;
}
lintFile(file, { target: 'quickToolsOutput' });
}
function openQuickEdit(file) {
if (!file) {
alert('File non disponibile');
return;
}
fetch('{{ route('admin.code-quality.file') }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
},
body: JSON.stringify({ file, action: 'read' })
})
.then(res => res.json())
.then(data => {
if (!data.success) {
alert(data.message || 'Impossibile aprire il file');
return;
}
const hints = [];
let mode = 'edit';
if (!data.writable) {
mode = 'preview';
hints.push('Il file non è scrivibile dall\'applicazione: aggiorna i permessi oppure modifica manualmente.');
}
openAutofixModal(data.file, data.content, hints, mode);
})
.catch(error => {
console.error(error);
alert('Errore apertura file: ' + error);
});
}
function openQuickEditFromInput() {
const file = document.getElementById('quickToolsFile').value.trim();
if (!file) {
alert('Inserisci un percorso file relativo.');
return;
}
openQuickEdit(file);
}
function showQualityOutput(message, variant = 'info', targetId = 'quickToolsOutput') {
const box = document.getElementById(targetId);
if (!box) {
alert(message);
return;
}
const colors = {
success: '#bbf7d0',
error: '#fecaca',
warning: '#fef3c7',
info: '#bfdbfe'
};
box.textContent = message;
box.classList.remove('hidden');
box.style.color = colors[variant] || colors.info;
}
function appendPath(p) {
const input = document.querySelector('#scanForm input[name="path"]');
const parts = input.value.split(',').map(s => s.trim()).filter(Boolean);
if (!parts.includes(p)) parts.push(p);
input.value = parts.join(',');
}
function startScan() {
const form = document.getElementById('scanForm');
const formData = new FormData(form);
// Mostra loading
document.getElementById('scanLoading').classList.remove('hidden');
document.getElementById('scanSummary').classList.add('hidden');
document.getElementById('scanResults').classList.add('hidden');
// Richiesta AJAX
fetch('{{ route('admin.code-quality.scan') }}', {
method: 'POST',
body: formData,
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(data => {
document.getElementById('scanLoading').classList.add('hidden');
if (data.success) {
displayResults(data.results, data.summary);
} else {
alert('Errore durante la scansione: ' + data.message);
}
})
.catch(error => {
document.getElementById('scanLoading').classList.add('hidden');
console.error('Error:', error);
alert('Errore durante la scansione');
});
}
function quickScanGesconImport() {
const form = document.getElementById('scanForm');
form.querySelector('select[name="module"]').value = 'gescon-import';
form.querySelector('select[name="menu"]').value = 'gescon-import';
startScan();
}
function displayResults(results, summary) {
// Aggiorna summary
document.getElementById('totalFiles').textContent = summary.total_scanned_files ?? summary.total_files;
document.getElementById('totalIssues').textContent = summary.total_issues;
document.getElementById('criticalCount').textContent = summary.by_severity.critical;
document.getElementById('errorCount').textContent = summary.by_severity.error;
document.getElementById('scanSummary').classList.remove('hidden');
renderRuleSummary(summary);
renderModuleMenuSummary(summary);
// Genera risultati dettagliati
const resultsContainer = document.getElementById('scanResults');
resultsContainer.innerHTML = '';
if (results.length === 0) {
resultsContainer.innerHTML = `
<div class="netgescon-card text-center">
<i class="fas fa-check-circle text-green-500 text-4xl mb-4"></i>
<h3 class="netgescon-subtitle text-green-600">Nessun Problema Rilevato!</h3>
<p class="netgescon-text">Il codice rispetta tutti gli standard NetGescon.</p>
</div>
`;
} else {
results.forEach(fileResult => {
const fileCard = createFileResultCard(fileResult);
resultsContainer.appendChild(fileCard);
});
}
document.getElementById('scanResults').classList.remove('hidden');
}
function createFileResultCard(fileResult) {
const card = document.createElement('div');
card.className = 'netgescon-card';
const severityIcons = {
critical: '🔴',
error: '🟠',
warning: '🟡',
info: '🔵'
};
const fileName = fileResult.file || '';
const safeFile = fileName.replace(/'/g, "\\'");
const lintable = /\.php$/i.test(fileName) || /\.blade\.php$/i.test(fileName);
const fixableRules = new Set(['layout_required','layout_forbidden_app','bootstrap_classes','deprecated_helpers']);
let issuesHtml = '';
fileResult.issues.forEach(issue => {
const canFix = fixableRules.has(issue.rule);
issuesHtml += `
<div class="border-l-4 border-${getSeverityColor(issue.severity)}-500 pl-4 py-3 mb-4 bg-gray-50">
<div class="flex items-center justify-between">
<h4 class="font-medium text-gray-800">
${severityIcons[issue.severity]} ${issue.name}
</h4>
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-500">Riga ${issue.line}</span>
${canFix ? `<button onclick="applyAutoFix('${safeFile}', '${issue.rule}')" class="netgescon-btn-small netgescon-btn-secondary"><i class="fas fa-magic"></i> Fix Auto</button>` : ''}
</div>
</div>
<p class="text-sm text-gray-600 mt-1">${issue.description}</p>
<div class="mt-2">
<p class="text-xs text-gray-500 mb-1">Codice trovato:</p>
<code class="text-xs bg-red-100 text-red-800 px-2 py-1 rounded">${issue.match}</code>
</div>
<div class="mt-2">
<p class="text-xs text-gray-500 mb-1">Suggerimento:</p>
<code class="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">${issue.suggestion}</code>
</div>
</div>
`;
});
card.innerHTML = `
<div class="flex items-center justify-between mb-4 flex-wrap gap-2">
<h3 class="netgescon-subtitle flex items-center gap-2">
<i class="fas fa-file-code"></i>
${fileName}
</h3>
<div class="flex items-center gap-2 flex-wrap">
<span class="netgescon-badge netgescon-badge-warning">
${fileResult.issues.length} problemi
</span>
${lintable ? `<button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-secondary" onclick="lintFile('${safeFile}')"><i class="fas fa-stethoscope mr-1"></i>Sintassi</button>` : ''}
<button type="button" class="netgescon-btn netgescon-btn-xs" onclick="openQuickEdit('${safeFile}')"><i class="fas fa-edit mr-1"></i>Editor</button>
</div>
</div>
<div class="space-y-4">
${issuesHtml}
</div>
`;
return card;
}
function getSeverityColor(severity) {
const colors = {
critical: 'red',
error: 'orange',
warning: 'yellow',
info: 'blue'
};
return colors[severity] || 'gray';
}
function applyAutoFix(file, rule) {
fetch('{{ route('admin.code-quality.autofix') }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
},
body: JSON.stringify({
file: file,
rule: rule
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Fix applicato con successo!');
startScan(); // Riavvia scansione
return;
}
// Se il server restituisce un contenuto proposto (file non scrivibile o anteprima), mostralo
if (data.proposed) {
openAutofixModal(file, data.proposed, data.permissionHints || [], 'preview');
return;
}
alert('Errore: ' + (data.message || 'Nessun fix disponibile'));
})
.catch(error => {
console.error('Error:', error);
alert('Errore durante il fix automatico');
});
}
function exportResults(format) {
const form = document.getElementById('scanForm');
const formData = new FormData(form);
formData.append('format', format);
fetch('{{ route('admin.code-quality.export') }}', {
method: 'POST',
body: formData,
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
}).then(async res => {
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = format === 'json' ? 'code-quality-report.json' : 'code-quality-report.csv';
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
}).catch(err => {
console.error(err);
alert('Errore durante l\'export');
});
}
function renderRuleSummary(summary) {
const container = document.getElementById('ruleSummary');
if (!summary.by_rule || Object.keys(summary.by_rule).length === 0) {
container.classList.add('hidden');
return;
}
container.classList.remove('hidden');
let html = '<div class="flex flex-wrap gap-2">';
Object.entries(summary.by_rule).forEach(([key, data]) => {
html += `<span class="netgescon-badge netgescon-badge-info" title="${data.name}">${data.name}: ${data.count}</span>`;
});
html += '</div>';
container.innerHTML = html;
}
function renderModuleMenuSummary(summary) {
const container = document.getElementById('moduleMenuSummary');
let html = '<div class="flex flex-wrap gap-3">';
if (summary.by_module) {
html += '<div><div class="font-medium mb-1">Per Modulo</div>';
Object.entries(summary.by_module).forEach(([k, v]) => {
html += `<span class="netgescon-badge netgescon-badge-secondary">${k}: ${v}</span> `;
});
html += '</div>';
}
if (summary.by_menu) {
html += '<div><div class="font-medium mb-1">Per Menù</div>';
Object.entries(summary.by_menu).forEach(([k, v]) => {
html += `<span class="netgescon-badge netgescon-badge-secondary">${k}: ${v}</span> `;
});
html += '</div>';
}
html += '</div>';
container.innerHTML = html;
}
// Avvia scansione automatica se ci sono già risultati
@if(isset($results))
document.addEventListener('DOMContentLoaded', function() {
const results = @json($results ?? []);
// Generiamo summary manualmente dal frontend se ci sono risultati
if (results.length > 0) {
const summary = generateSummaryFromResults(results);
displayResults(results, summary);
}
});
function generateSummaryFromResults(results) {
const summary = {
total_files: results.length,
total_scanned_files: {{ $totalScanned ?? 'null' }},
total_issues: 0,
by_severity: { critical: 0, error: 0, warning: 0, info: 0 },
by_rule: {},
by_module: {},
by_menu: {}
};
results.forEach(fileResult => {
fileResult.issues.forEach(issue => {
summary.total_issues++;
summary.by_severity[issue.severity]++;
if (!summary.by_rule[issue.rule]) {
summary.by_rule[issue.rule] = {
name: issue.name,
count: 0
};
}
summary.by_rule[issue.rule].count++;
const module = fileResult.module || 'unknown';
const menu = fileResult.menu || 'unknown';
summary.by_module[module] = (summary.by_module[module] || 0) + 1;
summary.by_menu[menu] = (summary.by_menu[menu] || 0) + 1;
});
});
return summary;
}
@endif
</script>
@endsection