[FEAT] Rischi #500 fase 2 (frontend): elenco rischi = requisiti Lex&ISO

La pagina Gestione Rischi mostra come tabella primaria (read-only) i requisiti
del framework Lex&ISO come rischi, codice RSK-<misura>Com.N via /api/risks/derived,
filtrati per classe (importante All.1 / essenziale All.2). Framing normativo
(validato nis2-expert): ogni voce = "rischio derivante dal mancato/parziale
soddisfacimento del requisito", mai "requisito = rischio"; RSK e "92" NON come
codici/numeri ufficiali ACN; matrice = strumento di rappresentazione (art.24).
- Rischi CUSTOM mantenuti in sezione "Rischi aggiuntivi (all-hazard)".
- Tasti "Nuovo Rischio"/"AI Suggerisci" limitati a super_admin/org_admin (non
  rimossi): l'inserimento libero serve al multirischio (art.24 D.Lgs.138/2024).
- api.js derivedRisks(); help.js + i18n aggiornati; version 1.25.4; SW v1.25.4.
- Cache-buster api/help/i18n ?v=20260727r. Backend/DB non toccati.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-07-27 14:15:03 +02:00
co-authored by Claude Opus 4.8
parent 571e151305
commit 4e7e75582a
46 changed files with 287 additions and 117 deletions
+155 -6
View File
@@ -331,12 +331,57 @@
<button class="btn-ghost" id="btn-ai-suggest" onclick="aiSuggest()">
<i data-lucide="sparkles" class="ic"></i>AI Suggerisci
</button>
<button class="btn-primary" onclick="showRiskModal()"><i data-lucide="plus" class="ic"></i>Nuovo Rischio</button>
<button class="btn-primary" id="btn-new-risk" onclick="showRiskModal()"><i data-lucide="plus" class="ic"></i>Nuovo Rischio</button>
</div>
</div>
<div class="content-body">
<!-- Table View -->
<!-- ── Nota introduttiva (framing normativo) ───────────── -->
<div class="risk-intro-note" style="display:flex;gap:12px;align-items:flex-start;padding:16px 18px;background:var(--gray-50);border:1px solid var(--gray-200);border-left:4px solid var(--primary);border-radius:var(--border-radius);margin-bottom:24px;">
<i data-lucide="info" class="ic" style="flex-shrink:0;color:var(--primary);margin-top:2px;"></i>
<p style="font-size:0.85rem;color:var(--gray-700);line-height:1.55;margin:0;">
In questa pagina i rischi corrispondono ai requisiti del framework di sicurezza applicabili alla tua organizzazione e alla loro valutazione. I requisiti derivano dalle misure di sicurezza di base della Determinazione ACN n. 164179 del 14/4/2025 (Allegato 1 per i soggetti importanti, Allegato 2 per gli essenziali). Ogni voce rappresenta il rischio derivante dal mancato o parziale soddisfacimento del requisito, non il requisito in sé. La valutazione e la matrice 5×5 sono uno strumento di rappresentazione: la normativa (art. 24 D.Lgs. 138/2024) richiede un'analisi dei rischi proporzionata e con approccio multirischio, senza imporre una metodologia specifica.
</p>
</div>
<!-- ── SEZIONE 1: Rischi derivati dai requisiti (PRIMARIA, read-only) ── -->
<div id="view-derived" style="margin-bottom:32px;">
<div style="display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap;gap:8px;margin-bottom:10px;">
<h2 style="font-size:1.05rem;margin:0;" data-i18n="risks.derived_heading">Rischi dai requisiti del framework di sicurezza</h2>
<span class="text-muted" style="font-size:0.78rem;" id="derived-source-note"></span>
</div>
<p class="text-muted" style="font-size:0.78rem;margin:0 0 12px;">
<span data-i18n="risks.derived_code_legend">RSK-‹misura›Com.‹n› è una codifica interna di NIS2 Agile; ‹misura› (es. GV.OC-04) è il codice della misura del Framework Nazionale richiamato dalla Determinazione ACN 164179/2025. Elenco in sola lettura.</span>
</p>
<div class="card">
<div class="table-container">
<table>
<thead>
<tr>
<th scope="col" data-i18n="risks.col_code">Codice</th>
<th scope="col" data-i18n="risks.col_risk">Rischio</th>
<th scope="col" data-i18n="risks.col_measure">Misura</th>
<th scope="col" data-i18n="risks.col_conformity">Conformità</th>
<th scope="col" data-i18n="risks.col_evaluation">Valutazione</th>
</tr>
</thead>
<tbody id="derived-table-body">
<tr>
<td colspan="5">
<div class="spinner" style="margin:40px auto;"></div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ── SEZIONE 2: Rischi aggiuntivi (multirischio / all-hazard) ── -->
<h2 style="font-size:1.05rem;margin:0 0 6px;" data-i18n="risks.custom_heading">Rischi aggiuntivi (approccio multirischio / all-hazard)</h2>
<p class="text-muted" style="font-size:0.82rem;margin:0 0 16px;line-height:1.5;" data-i18n="risks.custom_help">Oltre ai rischi derivati dai requisiti, i profili amministrativi possono aggiungere rischi non coperti da un singolo requisito (es. minacce fisiche, catena di fornitura, eventi naturali): copre l'approccio multirischio/all-hazard (art. 24 D.Lgs. 138/2024).</p>
<!-- Table View (rischi custom) -->
<div id="view-table">
<div class="filters-bar">
<select class="form-select" id="filter-category" onchange="loadRisks()" aria-label="Filtra per categoria">
@@ -426,6 +471,7 @@
</div>
</div>
</div>
<p class="text-muted" style="font-size:0.78rem;text-align:center;margin:16px auto 0;max-width:640px;line-height:1.5;" data-i18n="risks.matrix_caption">La matrice 5×5 è uno strumento di rappresentazione, non un obbligo normativo (art. 24 D.Lgs. 138/2024).</p>
</div>
</div>
</div>
@@ -437,12 +483,12 @@
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → pagina -->
<script src="/js/api.js?v=20260627u"></script>
<script src="/js/api.js?v=20260727r"></script>
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260627p"></script>
<script src="/js/help.js?v=20260727a"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script>
// ── Auth & Init ──────────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
@@ -489,11 +535,112 @@
// ── State ────────────────────────────────────────────────────
let currentView = 'table';
let risksData = [];
let derivedData = [];
let orgClass = null;
// ── Gating ruolo: creazione/AI riservate ai profili amministrativi ──
const ADMIN_ROLES = ['super_admin', 'org_admin'];
(function gateAdminActions() {
const role = (api.getUserRole && api.getUserRole()) || '';
if (!ADMIN_ROLES.includes(role)) {
['btn-ai-suggest', 'btn-new-risk'].forEach(id => {
const el = document.getElementById(id);
if (el) el.style.display = 'none';
});
}
})();
// ── Load ─────────────────────────────────────────────────────
loadDerivedRisks();
loadRisks();
loadMatrix();
// ── Derived Risks (requisiti del framework) ──────────────────
// Badge conformità per lo stato di valutazione del requisito.
const DERIVED_STATUS_BADGE = {
da_valutare: { cls: 'badge-neutral', label: 'Da valutare' },
non_applicabile: { cls: 'badge-neutral', label: 'Non applicabile' },
non_conforme: { cls: 'badge-danger', label: 'Non conforme' },
parziale: { cls: 'badge-warning', label: 'Parziale' },
conforme: { cls: 'badge-success', label: 'Conforme' },
};
function isDerivedApplicable(r) {
// important → applicabili solo i requisiti Allegato 1; essential → Allegato 2;
// classe non determinata / adesione volontaria → tutti applicabili.
if (orgClass === 'important') return r.applies_important == 1;
if (orgClass === 'essential') return r.applies_essential == 1;
return true;
}
async function loadDerivedRisks() {
try {
const result = await api.derivedRisks();
if (result && result.success && result.data) {
orgClass = result.data.org_class || null;
derivedData = result.data.risks || [];
const src = result.data.source || '';
const note = document.getElementById('derived-source-note');
if (note && src) note.textContent = 'Fonte: ' + src;
renderDerivedTable(derivedData);
} else {
renderDerivedTable([]);
}
} catch (e) {
renderDerivedTable([]);
}
}
function renderDerivedTable(rows) {
const tbody = document.getElementById('derived-table-body');
if (!rows || rows.length === 0) {
tbody.innerHTML = `
<tr><td colspan="5">
<div class="empty-state">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 1.944A11.954 11.954 0 012.166 5C2.056 5.649 2 6.319 2 7c0 5.225 3.34 9.67 8 11.317C14.66 16.67 18 12.225 18 7c0-.682-.057-1.35-.166-2.001A11.954 11.954 0 0110 1.944z" clip-rule="evenodd"/></svg>
<h4>Nessun requisito applicabile</h4>
<p>Non risultano requisiti del framework di sicurezza per questa organizzazione.</p>
</div>
</td></tr>`;
return;
}
let html = '';
rows.forEach(r => {
const applicable = isDerivedApplicable(r);
const st = DERIVED_STATUS_BADGE[r.stato] || DERIVED_STATUS_BADGE.da_valutare;
const measureCode = r.misura_code || '-';
const reqCode = r.requisito_code || '';
const reqDescr = r.requisito_descr || '';
const measureDescr = r.misura_descr || '';
const evalVal = (r.valutazione_rischio !== null && r.valutazione_rischio !== undefined && r.valutazione_rischio !== '')
? escapeHtml(String(r.valutazione_rischio)) : '—';
const rowStyle = applicable ? '' : 'style="opacity:0.5;"';
const riskTooltip = `Rischio derivante dal mancato o parziale soddisfacimento del requisito ${measureCode} ${reqCode}`.trim();
// Colonna "Rischio": etichetta + requisito come OGGETTO del rischio (mai il requisito nudo).
const conformityCell = applicable
? `<span class="badge ${st.cls}">${escapeHtml(st.label)}</span>`
: `<span class="badge badge-neutral">Non applicabile alla tua classe</span>`;
html += `
<tr ${rowStyle}>
<td><code style="font-size:0.78rem;color:var(--primary);">${escapeHtml(r.risk_code || '-')}</code></td>
<td title="${escapeHtml(riskTooltip)}">
<div style="font-size:0.68rem;text-transform:uppercase;letter-spacing:0.04em;font-weight:600;color:var(--gray-500);margin-bottom:2px;">Rischio di non conformità</div>
<div style="font-size:0.86rem;color:var(--gray-800);">${escapeHtml(reqDescr)}</div>
</td>
<td>
<code style="font-size:0.75rem;color:var(--gray-600);">${escapeHtml(measureCode)}</code>
${measureDescr ? `<div style="font-size:0.78rem;color:var(--gray-500);margin-top:2px;">${escapeHtml(measureDescr)}</div>` : ''}
</td>
<td>${conformityCell}</td>
<td>${evalVal}</td>
</tr>`;
});
tbody.innerHTML = html;
}
async function loadRisks() {
const params = {};
const cat = document.getElementById('filter-category').value;
@@ -666,6 +813,7 @@
}
function renderDetail(risk) {
document.getElementById('view-derived').classList.add('hidden');
document.getElementById('view-table').classList.add('hidden');
document.getElementById('view-matrix').classList.add('hidden');
const container = document.getElementById('view-detail');
@@ -839,7 +987,8 @@
function backToList() {
document.getElementById('view-detail').classList.add('hidden');
// Tabella e matrice sono entrambe sempre visibili.
// Sezione derivati, tabella custom e matrice sono sempre visibili.
document.getElementById('view-derived').classList.remove('hidden');
document.getElementById('view-table').classList.remove('hidden');
document.getElementById('view-matrix').classList.remove('hidden');
}