[FEAT] UI frontend gap competitivi + help/i18n + OpenAPI + scorecard EVIX

UI delle 4 feature backend (ora usabili dagli utenti, non solo via API):
- risks.html: vista 'Quantitativo (FAIR)' con form + istogramma distribuzione ALE + registro portafoglio; vista 'KRI' con dashboard semafori green/amber/red + CRUD
- reports.html: tab 'Monitoraggio Continuo' con semafori freschezza controlli (healthy/warning/stale/failing) + copertura
- assets.html: bottone 'Importa' CSV/CMDB con parsing client + anteprima + scoring auto GV.OC-04
- api.js: metodi computeFair/getFairRegister/listKri/createKri/updateKri/importAssets/getControlsMonitoring

Trasversale:
- help.js: sezioni guida FAIR+KRI (risks), import CMDB (assets), monitoraggio continuo (reports)
- i18n.js: chiavi IT/EN (risks.fair_tab, risks.kri_tab, assets.import_btn, audit.monitoring_tab)
- ServicesController::openapi esteso con incidents-ingest/evidence-ingest/assets-ingest/controls-monitoring + securityScheme ApiKeyAuth
- AuditController::controlsMonitoring (versione JWT per la UI) + route audit/controlsMonitoring
- EVIX scorecard: gap P1/P2 marcati chiusi (backend), roadmap aggiornata

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-05-30 09:52:21 +02:00
co-authored by Claude Opus 4.8
parent edf0394616
commit 372ccb5ec1
7 changed files with 357 additions and 20 deletions
+55 -1
View File
@@ -869,6 +869,60 @@
// ── Initial Load ────────────────────────────────────────
loadAssets();
</script>
/* ── Import asset CMDB/CSV (P2) ───────────────────────────── */
function openImportModal(){
const body = `
<p style="font-size:.85rem;color:var(--gray-600);margin-bottom:10px;">Incolla CSV o carica un file. Colonne riconosciute:
<code>name, asset_type, criticality, data_classification, internet_facing, dependencies_count, regulated, external_ref, vendor, location</code>.
Lo scoring di rilevanza NIS2 (GV.OC-04) viene calcolato automaticamente.</p>
<div class="form-group"><input type="file" accept=".csv,text/csv" id="imp-file" onchange="impFile(event)"></div>
<div class="form-group"><label class="form-label">Oppure incolla CSV (prima riga = intestazioni)</label>
<textarea class="form-input" id="imp-csv" rows="8" placeholder="name,asset_type,criticality,data_classification,internet_facing,dependencies_count,regulated
Core ERP,software,critical,financial,false,8,true
Web Portal,service,high,personal,true,3,true"></textarea></div>
<div id="imp-preview" style="font-size:.8rem;color:var(--gray-600);"></div>`;
showModal('Importa Asset', body, {
size:'lg',
footer:`<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="doImport()">Importa</button>`
});
}
function impFile(e){
const f=e.target.files[0]; if(!f)return;
const r=new FileReader(); r.onload=()=>{document.getElementById('imp-csv').value=r.result;}; r.readAsText(f);
}
function parseCsv(text){
const lines=text.trim().split(/\r?\n/).filter(l=>l.trim());
if(lines.length<2)return [];
const headers=lines[0].split(',').map(h=>h.trim().toLowerCase());
const truthy=v=>/^(1|true|si|sì|yes|y)$/i.test((v||'').trim());
return lines.slice(1).map(line=>{
const cells=line.split(','); const o={};
headers.forEach((h,i)=>{ o[h]=(cells[i]||'').trim(); });
const a={name:o.name, asset_type:o.asset_type, criticality:o.criticality,
data_classification:o.data_classification, vendor:o.vendor, location:o.location,
external_ref:o.external_ref};
if('internet_facing' in o) a.internet_facing=truthy(o.internet_facing);
if('regulated' in o) a.regulated=truthy(o.regulated);
if('dependencies_count' in o) a.dependencies_count=parseInt(o.dependencies_count)||0;
return a;
}).filter(a=>a.name);
}
async function doImport(){
const assets=parseCsv(document.getElementById('imp-csv').value);
if(!assets.length){showNotification('Nessuna riga valida (serve almeno name)','warning');return;}
try{
const res=await api.importAssets({source:'csv', assets});
if(res.success){
const d=res.data;
closeModal();
showNotification(`Import: ${d.imported} nuovi, ${d.updated} aggiornati, ${d.relevant} rilevanti NIS2, ${d.skipped} scartati`,'success');
if(typeof loadAssets==='function')loadAssets();
} else showNotification(res.message||'Errore import','error');
}catch(e){showNotification('Errore di connessione','error');}
}
</script>
</body>
</html>