#1 CRITICO aiAnalyze: askWithRag ritorna ['answer','sources','rag_used'], non una stringa. Ora estrae 'answer' (ai_summary) e salva 'sources' in ai_recommendations. Prima salvava il JSON intero in ai_summary. #2 ALTO corpus RAG: acn_requirements.json aveva 188/203 testi TRONCATI alla prima riga PDF (es. GV.PO-01#1: 84 char invece di 838). Rigenerato dai testi INTEGRALI di acn_measures.json (87+116, zero troncamenti). Ri-ingest Qdrant. #3 MEDIO catalog(): org non classificata dava entity_level=null + warning PHP $totals[null] + TypeError frontend. Ora 422 ENTITY_LEVEL_REQUIRED come create(). #4 MEDIO guida cap-5 GV.RR-04: "figure chiave dell'organigramma" era errato e auto-contraddittorio -> "personale autorizzato + amministratori di sistema, valutazione esperienza/capacita/affidabilita" (allineato testo ACN). #5 BASSI: openAcn try/catch (no unhandled rejection su Riprendi); badge importante/essenziale IT/EN; overall_score=null (non 0.0) se tutti N/A. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
414 lines
23 KiB
HTML
414 lines
23 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="it">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Gap Analysis ACN - NIS2 Agile</title>
|
||
<link rel="stylesheet" href="css/style.css">
|
||
<style>
|
||
.acn-intro { background:#eff6ff; border-left:4px solid var(--primary, #2563eb); padding:14px 18px; border-radius:8px; margin-bottom:20px; font-size:.92rem; line-height:1.6; }
|
||
.acn-level-badge { display:inline-block; padding:4px 12px; border-radius:999px; font-size:.8rem; font-weight:700; }
|
||
.acn-level-important { background:#fef3c7; color:#92400e; }
|
||
.acn-level-essential { background:#fee2e2; color:#991b1b; }
|
||
.acn-func { border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; margin-bottom:14px; overflow:hidden; }
|
||
.acn-func-head { background:var(--gray-50,#f9fafb); padding:12px 16px; cursor:pointer; display:flex; justify-content:space-between; align-items:center; gap:12px; }
|
||
.acn-func-head strong { font-size:1rem; }
|
||
.acn-func-meta { font-size:.8rem; color:var(--gray-500,#6b7280); }
|
||
.acn-func-body { padding:8px 16px 16px; display:none; }
|
||
.acn-func.open .acn-func-body { display:block; }
|
||
.acn-measure { border-top:1px solid var(--gray-100,#f3f4f6); padding:14px 0; }
|
||
.acn-measure-code { display:inline-block; background:#eef2ff; color:#3730a3; font-weight:700; font-size:.72rem; padding:2px 8px; border-radius:6px; margin-right:8px; }
|
||
.acn-measure-title { font-weight:600; font-size:.92rem; }
|
||
.acn-req { padding:12px 0 12px 14px; border-left:2px solid var(--gray-100,#f3f4f6); margin:10px 0 10px 8px; }
|
||
.acn-req-text { font-size:.9rem; margin-bottom:8px; }
|
||
.acn-req-idx { color:var(--gray-400,#9ca3af); font-weight:700; margin-right:6px; }
|
||
.acn-opts { display:flex; flex-wrap:wrap; gap:6px; }
|
||
.acn-opt { min-height:38px; padding:6px 12px; border:1.5px solid var(--gray-200,#e5e7eb); border-radius:20px; background:#fff; cursor:pointer; font-size:.84rem; font-weight:600; }
|
||
.acn-opt.sel-implemented { background:#16a34a; color:#fff; border-color:#16a34a; }
|
||
.acn-opt.sel-partial { background:#f59e0b; color:#fff; border-color:#f59e0b; }
|
||
.acn-opt.sel-not_implemented { background:#dc2626; color:#fff; border-color:#dc2626; }
|
||
.acn-opt.sel-not_applicable { background:#6b7280; color:#fff; border-color:#6b7280; }
|
||
.acn-progress { position:sticky; top:0; background:#fff; z-index:5; padding:12px 0; border-bottom:1px solid var(--gray-100,#f3f4f6); }
|
||
.acn-bar { height:8px; background:var(--gray-100,#f3f4f6); border-radius:4px; overflow:hidden; margin:8px 0; }
|
||
.acn-bar-fill { height:100%; background:var(--primary,#2563eb); width:0; transition:width .3s; }
|
||
.acn-func-score { display:flex; justify-content:space-between; padding:8px 0; border-bottom:1px solid var(--gray-100,#f3f4f6); font-size:.9rem; }
|
||
.acn-score-pill { font-weight:700; padding:2px 10px; border-radius:6px; }
|
||
.savehint { font-size:.78rem; color:var(--gray-400,#9ca3af); min-height:16px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app-layout">
|
||
<aside class="sidebar" id="sidebar"></aside>
|
||
<main class="main-content">
|
||
<header class="content-header">
|
||
<h2 data-i18n="acn.title">Gap Analysis ACN (misure di base)</h2>
|
||
<div class="content-header-actions">
|
||
<button class="btn btn-outline btn-sm" id="btn-ai-analyze" style="display:none;" onclick="acnAiAnalyze()" data-i18n="acn.ai">Analisi AI dei gap</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="content-body">
|
||
<div class="acn-intro" id="acn-intro">
|
||
<strong data-i18n="acn.intro.title">Conformità puntuale alla Determinazione ACN 164179/2025.</strong>
|
||
<span data-i18n="acn.intro.body">Questa analisi va oltre le 10 misure generiche dell'Art. 21: valuta le misure e i requisiti puntuali stabiliti dall'ACN per i soggetti NIS. Il perimetro dipende dalla classificazione del soggetto (importante o essenziale).</span>
|
||
</div>
|
||
|
||
<!-- START -->
|
||
<div id="acn-start" class="card mb-24">
|
||
<div class="card-body" style="padding:32px; text-align:center;">
|
||
<div id="acn-level-info" class="mb-24"></div>
|
||
<div id="acn-existing" class="mb-24" style="display:none; text-align:left;"></div>
|
||
<button class="btn btn-primary btn-lg" id="btn-new-acn" onclick="createAcn()" data-i18n="acn.new">Nuova Gap Analysis ACN</button>
|
||
<div style="margin-top:14px;">
|
||
<button class="btn btn-outline btn-sm" onclick="showCatalog()" data-i18n="acn.catalog.btn">Consulta il catalogo dei requisiti</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CATALOG (read-only) -->
|
||
<div id="acn-catalog" class="card mb-24" style="display:none;">
|
||
<div class="card-header"><h3 data-i18n="acn.catalog.title">Catalogo requisiti ACN applicabili</h3></div>
|
||
<div class="card-body" id="acn-catalog-body"></div>
|
||
</div>
|
||
|
||
<!-- WIZARD -->
|
||
<div id="acn-wizard" style="display:none;">
|
||
<div class="card acn-progress">
|
||
<div class="card-body" style="padding:14px 18px;">
|
||
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap;">
|
||
<strong id="acn-wizard-title">Gap Analysis ACN</strong>
|
||
<span id="acn-wizard-level"></span>
|
||
</div>
|
||
<div class="acn-bar"><div class="acn-bar-fill" id="acn-bar-fill"></div></div>
|
||
<div style="font-size:.82rem; color:var(--gray-500,#6b7280);"><span id="acn-answered">0</span> / <span id="acn-total">0</span> <span data-i18n="acn.answered">requisiti con risposta</span></div>
|
||
<div class="savehint" id="acn-savehint"></div>
|
||
</div>
|
||
</div>
|
||
<div class="card mb-24">
|
||
<div class="card-body" id="acn-functions"></div>
|
||
</div>
|
||
<div class="card mb-24">
|
||
<div class="card-body" style="text-align:center;">
|
||
<div id="acn-wizard-msg"></div>
|
||
<button class="btn btn-primary btn-lg" id="btn-acn-complete" onclick="completeAcn()" data-i18n="acn.complete">Calcola punteggio e completa</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- RESULTS -->
|
||
<div id="acn-results" style="display:none;">
|
||
<div class="card mb-24">
|
||
<div class="card-header"><h3 data-i18n="acn.results.title">Esito Gap Analysis ACN</h3></div>
|
||
<div class="card-body">
|
||
<div style="text-align:center; margin-bottom:20px;">
|
||
<div style="font-size:3rem; font-weight:800;" id="acn-overall">0</div>
|
||
<div class="text-muted" data-i18n="acn.results.overall">Punteggio complessivo di conformità (0-100)</div>
|
||
</div>
|
||
<h4 style="margin:16px 0 8px;" data-i18n="acn.results.byfunc">Punteggio per funzione (Framework Nazionale)</h4>
|
||
<div id="acn-func-scores"></div>
|
||
</div>
|
||
</div>
|
||
<div class="card mb-24" id="acn-ai-card" style="display:none;">
|
||
<div class="card-header"><h3 data-i18n="acn.results.ai">Analisi AI dei gap</h3></div>
|
||
<div class="card-body" id="acn-ai-body"></div>
|
||
</div>
|
||
<div class="card mb-24">
|
||
<div class="card-header"><h3 data-i18n="acn.results.gaps">Requisiti non conformi (piano d'azione)</h3></div>
|
||
<div class="card-body" id="acn-gaps"></div>
|
||
</div>
|
||
<div style="text-align:center; margin-bottom:24px;">
|
||
<button class="btn btn-outline" onclick="location.reload()" data-i18n="acn.backstart">Torna all'inizio</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<script src="js/i18n.js"></script>
|
||
<script src="js/common.js"></script>
|
||
<script src="js/api.js"></script>
|
||
<script src="js/help.js"></script>
|
||
<script>
|
||
'use strict';
|
||
const FUNC_LABELS = {
|
||
GOVERN: { it:'Governo (GV)', en:'Govern (GV)' },
|
||
IDENTIFY: { it:'Identificazione (ID)', en:'Identify (ID)' },
|
||
PROTECT: { it:'Protezione (PR)', en:'Protect (PR)' },
|
||
DETECT: { it:'Rilevamento (DE)', en:'Detect (DE)' },
|
||
RESPOND: { it:'Risposta (RS)', en:'Respond (RS)' },
|
||
RECOVER: { it:'Ripristino (RC)', en:'Recover (RC)' }
|
||
};
|
||
let ACN = { id:null, level:null, answers:{}, total:0 };
|
||
let saveTimer = null;
|
||
|
||
function lang(){ try { return I18n.getLang ? I18n.getLang() : 'it'; } catch(e){ return 'it'; } }
|
||
function el(id){ return document.getElementById(id); }
|
||
function esc(s){ const d=document.createElement('div'); d.textContent=s==null?'':String(s); return d.innerHTML; }
|
||
|
||
document.addEventListener('DOMContentLoaded', async function(){
|
||
if (typeof checkAuth==='function' && !checkAuth()) return;
|
||
if (window.I18n && I18n.init) I18n.init('it');
|
||
if (typeof loadSidebar==='function') loadSidebar();
|
||
if (window.HelpSystem && HelpSystem.init) HelpSystem.init();
|
||
await initStart();
|
||
});
|
||
|
||
async function initStart(){
|
||
try {
|
||
const cat = await api.acnCatalog();
|
||
ACN.level = cat.entity_level;
|
||
const lvlTxt = cat.entity_level === 'essential'
|
||
? '<span class="acn-level-badge acn-level-essential">'+(lang()==='en'?'ESSENTIAL entity':'Soggetto ESSENZIALE')+'</span>'
|
||
: '<span class="acn-level-badge acn-level-important">'+(lang()==='en'?'IMPORTANT entity':'Soggetto IMPORTANTE')+'</span>';
|
||
el('acn-level-info').innerHTML = lvlTxt +
|
||
'<p class="text-muted" style="margin-top:10px;">' +
|
||
(lang()==='en'?'Applicable scope: ':'Perimetro applicabile: ') +
|
||
'<strong>'+cat.totals.measures+'</strong> '+(lang()==='en'?'measures':'misure')+', <strong>'+
|
||
cat.totals.requirements+'</strong> '+(lang()==='en'?'requirements':'requisiti')+'.</p>';
|
||
window._acnCatalog = cat;
|
||
} catch(e){
|
||
if (e && e.error_code === 'ENTITY_LEVEL_REQUIRED') {
|
||
el('acn-level-info').innerHTML = '<p class="text-muted">'+(lang()==='en'
|
||
?'Classify the organization as important or essential first.'
|
||
:'Classifica prima il soggetto come importante o essenziale (Impostazioni › Classificazione NIS2).')+'</p>';
|
||
} else {
|
||
el('acn-level-info').innerHTML = '<p class="text-muted">'+(lang()==='en'?'Could not load.':'Caricamento non riuscito.')+'</p>';
|
||
}
|
||
}
|
||
try {
|
||
const list = await api.acnList();
|
||
if (list.assessments && list.assessments.length){
|
||
let html = '<h4 style="margin-bottom:8px;">'+(lang()==='en'?'Existing assessments':'Assessment esistenti')+'</h4>';
|
||
list.assessments.forEach(function(a){
|
||
const st = a.status==='completed'
|
||
? '<span class="badge badge-success">'+(lang()==='en'?'Completed':'Completato')+(a.overall_score!=null?(' · '+a.overall_score):'')+'</span>'
|
||
: '<span class="badge badge-warning">'+(lang()==='en'?'In progress':'In corso')+'</span>';
|
||
html += '<div style="display:flex; justify-content:space-between; align-items:center; gap:10px; padding:8px 0; border-bottom:1px solid var(--gray-100,#f3f4f6);">'+
|
||
'<div><strong>'+esc(a.title)+'</strong> '+st+'</div>'+
|
||
'<button class="btn btn-sm btn-outline" onclick="openAcn('+a.id+',\''+a.status+'\')">'+
|
||
(a.status==='completed'?(lang()==='en'?'View':'Vedi'):(lang()==='en'?'Resume':'Riprendi'))+'</button></div>';
|
||
});
|
||
el('acn-existing').innerHTML = html;
|
||
el('acn-existing').style.display='block';
|
||
}
|
||
} catch(e){}
|
||
}
|
||
|
||
async function showCatalog(){
|
||
const cat = window._acnCatalog;
|
||
if (!cat){ return; }
|
||
let html = '';
|
||
(cat.functions||[]).forEach(function(f){
|
||
html += '<div class="acn-func open"><div class="acn-func-head" onclick="this.parentNode.classList.toggle(\'open\')">'+
|
||
'<strong>'+esc(FUNC_LABELS[f.function_code]?FUNC_LABELS[f.function_code][lang()]:f.function_name)+'</strong>'+
|
||
'<span class="acn-func-meta">'+(f.measures||[]).length+' '+(lang()==='en'?'measures':'misure')+'</span></div>'+
|
||
'<div class="acn-func-body">';
|
||
(f.measures||[]).forEach(function(m){
|
||
html += '<div class="acn-measure"><span class="acn-measure-code">'+esc(m.measure_code)+'</span>'+
|
||
'<span class="acn-measure-title">'+esc(m.measure_title)+'</span>';
|
||
(m.requirements||[]).forEach(function(r){
|
||
html += '<div class="acn-req"><span class="acn-req-idx">'+r.index+'.</span>'+esc(r.text)+'</div>';
|
||
});
|
||
html += '</div>';
|
||
});
|
||
html += '</div></div>';
|
||
});
|
||
el('acn-catalog-body').innerHTML = html;
|
||
el('acn-catalog').style.display='block';
|
||
el('acn-catalog').scrollIntoView({behavior:'smooth'});
|
||
}
|
||
|
||
async function createAcn(){
|
||
try {
|
||
const res = await api.acnCreate({});
|
||
await openAcn(res.id, 'draft');
|
||
} catch(e){
|
||
alert((e && e.message) || (lang()==='en'?'Creation failed':'Creazione non riuscita'));
|
||
}
|
||
}
|
||
|
||
async function openAcn(id, status){
|
||
try {
|
||
ACN.id = id;
|
||
const res = await api.acnRequirements(id);
|
||
ACN.level = res.assessment.entity_level;
|
||
if (res.assessment.status === 'completed'){
|
||
await showResults(id);
|
||
return;
|
||
}
|
||
renderWizard(res);
|
||
} catch(e){
|
||
alert((e && e.message) || (lang()==='en'?'Could not open the assessment.':'Impossibile aprire l\'assessment.'));
|
||
}
|
||
}
|
||
|
||
function renderWizard(res){
|
||
el('acn-start').style.display='none';
|
||
el('acn-catalog').style.display='none';
|
||
el('acn-results').style.display='none';
|
||
el('acn-wizard').style.display='block';
|
||
el('btn-ai-analyze').style.display='none';
|
||
el('acn-wizard-level').innerHTML = ACN.level==='essential'
|
||
? '<span class="acn-level-badge acn-level-essential">'+(lang()==='en'?'ESSENTIAL':'ESSENZIALE')+'</span>'
|
||
: '<span class="acn-level-badge acn-level-important">'+(lang()==='en'?'IMPORTANT':'IMPORTANTE')+'</span>';
|
||
|
||
ACN.answers = {}; ACN.total = 0;
|
||
let html = '';
|
||
(res.functions||[]).forEach(function(f, fi){
|
||
let fcount = 0;
|
||
let inner = '';
|
||
(f.categories||[]).forEach(function(c){
|
||
(c.measures||[]).forEach(function(m){
|
||
inner += '<div class="acn-measure"><span class="acn-measure-code">'+esc(m.measure_code)+'</span>'+
|
||
'<span class="acn-measure-title">'+esc(m.measure_title)+'</span>';
|
||
(m.requirements||[]).forEach(function(r){
|
||
ACN.total++;
|
||
fcount++;
|
||
if (r.response) ACN.answers[r.key] = r.response;
|
||
inner += renderReq(r);
|
||
});
|
||
inner += '</div>';
|
||
});
|
||
});
|
||
html += '<div class="acn-func'+(fi===0?' open':'')+'">'+
|
||
'<div class="acn-func-head" onclick="this.parentNode.classList.toggle(\'open\')">'+
|
||
'<strong>'+esc(FUNC_LABELS[f.function_code]?FUNC_LABELS[f.function_code][lang()]:f.function_name)+'</strong>'+
|
||
'<span class="acn-func-meta">'+fcount+' '+(lang()==='en'?'requirements':'requisiti')+'</span></div>'+
|
||
'<div class="acn-func-body">'+inner+'</div></div>';
|
||
});
|
||
el('acn-functions').innerHTML = html;
|
||
el('acn-total').textContent = ACN.total;
|
||
updateProgress();
|
||
}
|
||
|
||
function renderReq(r){
|
||
const opts = [
|
||
['implemented', lang()==='en'?'Implemented':'Attuato'],
|
||
['partial', lang()==='en'?'Partial':'Parziale'],
|
||
['not_implemented', lang()==='en'?'Not implemented':'Non attuato'],
|
||
['not_applicable', lang()==='en'?'N/A':'Non applic.']
|
||
];
|
||
let btns = '';
|
||
opts.forEach(function(o){
|
||
const sel = r.response===o[0] ? ('sel-'+o[0]) : '';
|
||
btns += '<button type="button" class="acn-opt '+sel+'" data-key="'+esc(r.key)+'" data-val="'+o[0]+'" onclick="pick(this)">'+o[1]+'</button>';
|
||
});
|
||
return '<div class="acn-req"><div class="acn-req-text"><span class="acn-req-idx">'+r.index+'.</span>'+esc(r.text)+'</div>'+
|
||
'<div class="acn-opts">'+btns+'</div></div>';
|
||
}
|
||
|
||
function pick(btn){
|
||
const key = btn.getAttribute('data-key');
|
||
const val = btn.getAttribute('data-val');
|
||
const wrap = btn.parentNode;
|
||
Array.prototype.forEach.call(wrap.querySelectorAll('.acn-opt'), function(b){
|
||
b.className = 'acn-opt';
|
||
});
|
||
btn.className = 'acn-opt sel-'+val;
|
||
ACN.answers[key] = val;
|
||
updateProgress();
|
||
scheduleSave(key, val);
|
||
}
|
||
|
||
let pendingSaves = {};
|
||
function scheduleSave(key, val){
|
||
pendingSaves[key] = val;
|
||
el('acn-savehint').textContent = lang()==='en'?'Saving…':'Salvataggio…';
|
||
if (saveTimer) clearTimeout(saveTimer);
|
||
saveTimer = setTimeout(flushSaves, 700);
|
||
}
|
||
async function flushSaves(){
|
||
const batch = Object.keys(pendingSaves).map(function(k){ return { requirement_key:k, response_value:pendingSaves[k] }; });
|
||
if (!batch.length) return;
|
||
pendingSaves = {};
|
||
try {
|
||
await api.acnRespond(ACN.id, { responses: batch });
|
||
el('acn-savehint').textContent = (lang()==='en'?'Saved ':'Salvato ')+new Date().toLocaleTimeString(lang()==='en'?'en-GB':'it-IT').slice(0,5);
|
||
} catch(e){
|
||
el('acn-savehint').textContent = lang()==='en'?'Save pending':'Salvataggio in sospeso';
|
||
}
|
||
}
|
||
|
||
function updateProgress(){
|
||
const answered = Object.keys(ACN.answers).length;
|
||
el('acn-answered').textContent = answered;
|
||
el('acn-bar-fill').style.width = (ACN.total? Math.round(answered/ACN.total*100):0)+'%';
|
||
}
|
||
|
||
async function completeAcn(){
|
||
await flushSaves();
|
||
const answered = Object.keys(ACN.answers).length;
|
||
if (answered < ACN.total){
|
||
el('acn-wizard-msg').innerHTML = '<div class="alert alert-warning">'+
|
||
(lang()==='en'?('Missing '+(ACN.total-answered)+' answers.'):('Mancano '+(ACN.total-answered)+' risposte.'))+'</div>';
|
||
return;
|
||
}
|
||
try {
|
||
await api.acnComplete(ACN.id);
|
||
await showResults(ACN.id);
|
||
} catch(e){
|
||
el('acn-wizard-msg').innerHTML = '<div class="alert alert-warning">'+esc((e&&e.message)||'Errore')+'</div>';
|
||
}
|
||
}
|
||
|
||
async function showResults(id){
|
||
const rep = await api.acnReport(id);
|
||
const a = rep.assessment;
|
||
el('acn-start').style.display='none';
|
||
el('acn-wizard').style.display='none';
|
||
el('acn-catalog').style.display='none';
|
||
el('acn-results').style.display='block';
|
||
el('btn-ai-analyze').style.display='inline-flex';
|
||
ACN.id = id;
|
||
el('acn-overall').textContent = a.overall_score!=null ? a.overall_score : '—';
|
||
el('acn-overall').style.color = scoreColor(a.overall_score);
|
||
|
||
let fs = '';
|
||
const fscores = a.function_scores || {};
|
||
Object.keys(FUNC_LABELS).forEach(function(fc){
|
||
if (fscores[fc]==null) return;
|
||
fs += '<div class="acn-func-score"><span>'+esc(FUNC_LABELS[fc][lang()])+'</span>'+
|
||
'<span class="acn-score-pill" style="background:'+scoreBg(fscores[fc])+'; color:'+scoreColor(fscores[fc])+';">'+fscores[fc]+'</span></div>';
|
||
});
|
||
el('acn-func-scores').innerHTML = fs;
|
||
|
||
let g = '';
|
||
(rep.gaps||[]).forEach(function(gap){
|
||
const tag = gap.response_value==='not_implemented'
|
||
? '<span class="badge badge-danger">'+(lang()==='en'?'Not implemented':'Non attuato')+'</span>'
|
||
: '<span class="badge badge-warning">'+(lang()==='en'?'Partial':'Parziale')+'</span>';
|
||
g += '<div style="padding:10px 0; border-bottom:1px solid var(--gray-100,#f3f4f6);">'+
|
||
'<span class="acn-measure-code">'+esc(gap.measure_code)+'</span> '+tag+
|
||
'<div style="font-size:.88rem; margin-top:4px;">'+esc(gap.requirement_text)+'</div></div>';
|
||
});
|
||
el('acn-gaps').innerHTML = g || '<p class="text-muted">'+(lang()==='en'?'No gaps. Fully compliant.':'Nessun gap: pienamente conforme.')+'</p>';
|
||
|
||
if (a.ai_summary){
|
||
el('acn-ai-body').innerHTML = '<div style="white-space:pre-wrap; font-size:.9rem;">'+esc(a.ai_summary)+'</div>';
|
||
el('acn-ai-card').style.display='block';
|
||
}
|
||
}
|
||
|
||
async function acnAiAnalyze(){
|
||
const btn = el('btn-ai-analyze');
|
||
btn.disabled = true;
|
||
const old = btn.textContent;
|
||
btn.textContent = lang()==='en'?'Analyzing…':'Analisi in corso…';
|
||
try {
|
||
const res = await api.acnAiAnalyze(ACN.id);
|
||
el('acn-ai-body').innerHTML = '<div style="white-space:pre-wrap; font-size:.9rem;">'+esc(res.ai_summary)+'</div>';
|
||
el('acn-ai-card').style.display='block';
|
||
el('acn-ai-card').scrollIntoView({behavior:'smooth'});
|
||
} catch(e){
|
||
alert((e&&e.message)||(lang()==='en'?'AI unavailable':'AI non disponibile'));
|
||
} finally {
|
||
btn.disabled=false; btn.textContent=old;
|
||
}
|
||
}
|
||
|
||
function scoreColor(s){ if(s==null)return '#6b7280'; if(s>=80)return '#16a34a'; if(s>=60)return '#65a30d'; if(s>=40)return '#f59e0b'; return '#dc2626'; }
|
||
function scoreBg(s){ if(s==null)return '#f3f4f6'; if(s>=80)return '#f0fdf4'; if(s>=60)return '#f7fee7'; if(s>=40)return '#fffbeb'; return '#fef2f2'; }
|
||
</script>
|
||
</body>
|
||
</html>
|