[FEAT] Gestione Rischi: tabella 12 colonne (inerente+residuo) — integrazione prototipo "Modulo Azioni" (Simon)

Integra la spec di Simon per la pagina Rischi con le correzioni normative
(parere nis2-expert + iso-27001-expert) applicate come eccezioni motivate:
- 12 colonne: Codice (link → Misure e Requisiti), Descrizione (RISCHIO DEFAULT =
  cfg_nis2_rischi.risk_descr), VALUTAZIONE RISCHIO INERENTE (P/I/PxI), Alert,
  Rischio Inerente, VALUTAZIONE RISCHIO RESIDUO (P/I/PxI), Esito residuo, Rischio Residuo.
- Editing INLINE di Probabilità/Impatto (1-5), salvataggio automatico.
- Alert a bande (15-25 Prioritaria / 9-14 Critica / 3-8 Necessaria / 1-2 Suggerita)
  → popup azioni + "crea azione" (tabella requisito_actions, seme Modulo Azioni).
- Eccezione #1: il rischio RESIDUO NON aggiorna in automatico lo STATO DI CONFORMITÀ del
  requisito (piani distinti). Col.11 "Esito residuo" = flag Rivalutare/Adeguato; conformità esplicita.
- Eccezione #3: banda 13-14 inclusa in "Critica" (9-14). "Danno €" → "Impatto" (1-5).
  Soglie = metodo interno (ISO 27001 §6.1.2), non obbligo (help/UI lo dichiarano).

Backend: mig 067; derivedList (+risk_descr +residuo), setState (+residuo), requisitoActions list/create.
help.js + i18n IT/EN. Cache-buster api/i18n/help 20260730a. Prossima mig=068.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-07-30 10:49:52 +02:00
co-authored by Claude Opus 4.8
parent cf224480cc
commit 8a0bc5ca39
49 changed files with 535 additions and 180 deletions
+1 -1
View File
@@ -70,7 +70,7 @@
</script>
<!-- Stessa logica della app: api.js + common.js (helper) + common-bi.js (override BI) -->
<script src="js/api.js?v=20260729b"></script>
<script src="js/api.js?v=20260730a"></script>
<script src="js/common.js?v=20260729b"></script>
<script src="js/common-bi.js?v=20260625d"></script>
<script>
+3 -3
View File
@@ -162,12 +162,12 @@
</main>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide → topnav-v3.js (chrome) → i18n → init pagina -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+3 -3
View File
@@ -85,12 +85,12 @@
</main>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide → topnav-v3.js (chrome) → i18n → init pagina -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+3 -3
View File
@@ -92,12 +92,12 @@
</main>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide → topnav-v3.js (chrome) → i18n → init pagina -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+3 -3
View File
@@ -1122,12 +1122,12 @@
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); // = loadTopnavV3()
+3 -3
View File
@@ -270,12 +270,12 @@
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help. La logica
pagina (wizard GAP) resta inline: usa gli stessi id/funzioni di prima. -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+3 -3
View File
@@ -377,12 +377,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=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
// ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+3 -3
View File
@@ -190,12 +190,12 @@
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help. La logica del
calendario (aggregatore read-only) resta inline e usa gli stessi id/funzioni. -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
'use strict';
/*
+3 -3
View File
@@ -197,12 +197,12 @@
</main>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide → topnav-v3.js (chrome) → i18n → help → init pagina -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
if (!checkAuth()) throw new Error('Not authenticated');
+3 -3
View File
@@ -192,12 +192,12 @@
</div>
<!-- Scripts V3. competenze.js si auto-inizializza su DOMContentLoaded (checkAuth/loadSidebar/I18n/Help). -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script src="/js/competenze.js?v=20260627"></script>
<script>
function v3Icons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } }
+3 -3
View File
@@ -98,12 +98,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=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script src="/js/connettori-discovery.js?v=20260627"></script>
<script>
if (typeof checkAuth === 'function' && !checkAuth()) {
+3 -3
View File
@@ -155,12 +155,12 @@
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
'use strict';
/* Controlli periodici (control monitoring §9.1/A.8.16): controlli ricorrenti + esecuzioni (evidenza) + NC/AC. */
+3 -3
View File
@@ -222,12 +222,12 @@
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
// ── Icone inline (Lucide stroke) usate nelle stringhe HTML generate dal JS.
// Renderizzate immediatamente (no createIcons re-run), self-host, niente sprite BI. ──
+3 -3
View File
@@ -112,12 +112,12 @@
<!-- Scripts V3: api.js (client REST, prima) → common.js (helper+FAB reali) →
Lucide self-host → topnav-v3.js (chrome, ridefinisce loadSidebar) →
i18n → help → dashboard.js -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script src="/js/dashboard.js?v=20260724a"></script>
<script>
// ── Auth check ───────────────────────────────────────────
+1 -1
View File
@@ -73,7 +73,7 @@
</script>
<!-- Stessa logica della forgot-password.html: ZERO modifiche backend -->
<script src="js/api.js?v=20260729b"></script>
<script src="js/api.js?v=20260730a"></script>
<script>
const form = document.getElementById('forgot-form');
const err = document.getElementById('err');
+3 -3
View File
@@ -1177,12 +1177,12 @@
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
// La guida è accessibile anche da non autenticati (utile per onboarding)
// ma se sei loggato carichi chrome + i18n normalmente. ZERO modifiche backend.
+3 -3
View File
@@ -359,12 +359,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=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
// ── Auth & Init ──────────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+2
View File
@@ -344,6 +344,8 @@ $actionMap = [
'GET:kri' => 'listKri', // P2: dashboard KRI
'POST:kri' => 'createKri',
'PUT:kri/{subId}' => 'updateKri',
'GET:requisitoActions' => 'requisitoActionsList', // #500/Modulo Azioni: azioni sul rischio-da-requisito
'POST:requisitoActions' => 'requisitoActionsCreate',
],
// ── IncidentController ──────────────────────────
+3 -3
View File
@@ -931,12 +931,12 @@ curl -H <span class="str">"X-API-Key: nis2_TUA_CHIAVE"</span> \
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); // = loadTopnavV3()
+3 -3
View File
@@ -160,12 +160,12 @@
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
'use strict';
/*
+3 -3
View File
@@ -199,12 +199,12 @@
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → isms.js.
isms.js si auto-inizializza su DOMContentLoaded (checkAuth/I18n/loadSidebar/Help/render). -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script src="/js/isms.js?v=20260627v3"></script>
<script>
// Icone Lucide della chrome + crumb/page-head statici. L'init dati e il wiring
+2
View File
@@ -451,6 +451,8 @@ class NIS2API {
deleteRisk(id) { return this.del(`/risks/${id}`); }
getRiskMatrix() { return this.get('/risks/matrix'); }
derivedRisks() { return this.get('/risks/derived'); }
requisitoActions(requisitoId) { return this.get('/risks/requisitoActions?requisito_id=' + encodeURIComponent(requisitoId)); }
createRequisitoAction(d) { return this.post('/risks/requisitoActions', d || {}); }
aiSuggestRisks() { return this.post('/risks/ai-suggest', {}); }
// FAIR quantitativo + KRI (P2)
computeFair(id, data) { return this.post(`/risks/${id}/fair`, data); }
+6 -3
View File
@@ -606,9 +606,12 @@ const HelpSystem = (function () {
items: [
'La <strong>prima tabella</strong> elenca i rischi <strong>derivati dai requisiti</strong> del framework di sicurezza: ogni voce e\' il <strong>rischio di non conformita\'</strong> legato al mancato o parziale soddisfacimento di un requisito, <strong>non il requisito in se\'</strong>. E\' un elenco in <strong>sola lettura</strong>.',
'Il codice <strong>RSK-‹misura›Com.‹n›</strong> e\' una <strong>codifica interna di NIS2 Agile</strong>; ‹misura› (es. GV.OC-04) e\' il codice della misura del Framework Nazionale richiamato dalla Determinazione ACN 164179/2025. <strong>Non</strong> e\' un codice o numero ufficiale ACN.',
'La colonna <strong>Conformita\'</strong> mostra lo stato di valutazione del requisito (Da valutare, Non conforme, Parziale, Conforme). La tabella elenca <strong>solo i requisiti applicabili alla tua classe</strong> (importante &rarr; Allegato 1, essenziale &rarr; Allegato 2 della Determinazione ACN 164179/2025): quelli non pertinenti non vengono mostrati. Se l\'elenco e\' lungo, il riquadro <strong>scorre verticalmente</strong> con l\'intestazione fissa.',
'La seconda tabella, <strong>Rischi aggiuntivi (approccio multirischio / all-hazard)</strong>, permette ai <strong>profili amministrativi</strong> (org_admin, super_admin) di aggiungere rischi non coperti da un singolo requisito (minacce fisiche, catena di fornitura, eventi naturali): copre l\'approccio multirischio richiesto dall\'art. 24 D.Lgs. 138/2024.',
'I profili <strong>Amministratore</strong>, <strong>Compliance Manager</strong> e <strong>super admin</strong> possono <strong>valutare</strong> ogni rischio-da-requisito con il pulsante <strong>Valuta</strong>: si impostano lo <strong>stato di conformità</strong>, la <strong>Probabilità</strong> e l\'<strong>Impatto</strong> (scala 1–5). La <strong>Probabilità</strong> è la probabilità che la minaccia si concretizzi <strong>sfruttando la carenza</strong> del requisito; l\'<strong>Impatto</strong> è l\'impatto della <strong>non conformità o del conseguente incidente</strong> — non la "probabilità del requisito". Il punteggio Rischio (Probabilità × Impatto) compare nella tabella e alimenta la matrice.'
'La tabella ha <strong>12 colonne</strong>: <strong>Codice rischio</strong> (cliccabile, apre la voce in Misure e Requisiti), <strong>Descrizione rischio</strong> (il rischio di default associato al requisito nel framework), e due valutazioni affiancate — <strong>RISCHIO INERENTE</strong> (prima del trattamento) e <strong>RISCHIO RESIDUO</strong> (dopo il trattamento). Ogni valutazione ha Probabilità, Impatto e Valutazione (= Probabilità × Impatto, scala 1–25).',
'I profili <strong>Amministratore</strong>, <strong>Compliance Manager</strong> e <strong>super admin</strong> compilano <strong>direttamente in tabella</strong> Probabilità e Impatto (1–5): la Valutazione si calcola da sola e il salvataggio è automatico. La <strong>Probabilità</strong> è la probabilità che la minaccia sfrutti la carenza del requisito; l\'<strong>Impatto</strong> (scala 1–5, non euro) è la severità della non conformità o del conseguente incidente.',
'La colonna <strong>Alert</strong> compare in base al punteggio inerente: <em>Azione Prioritaria</em> (15–25), <em>Azione Critica</em> (9–14), <em>Azione Necessaria</em> (3–8), <em>Azione Suggerita</em> (1–2). Le soglie sono una <strong>scelta metodologica</strong> dell\'organizzazione (rif. ISO/IEC 27001 §6.1.2), <strong>non un obbligo di legge</strong>. Cliccando l\'Alert si apre l\'elenco delle azioni passate su quel rischio e si può <strong>creare una nuova azione</strong>.',
'La colonna <strong>Esito residuo</strong> segnala se il rischio residuo va <strong>rivalutato</strong> (punteggio 9–25) o è <strong>adeguato</strong> (1–8). <strong>Importante</strong>: il punteggio di rischio residuo <strong>non</strong> determina automaticamente lo <em>stato di conformità</em> del requisito. La <strong>conformità</strong> dipende dall\'attuazione della misura e dalle evidenze e si aggiorna in modo esplicito dal link "stato" nella colonna Rischio Residuo. Sono due piani distinti (punteggio di rischio ≠ conformità al requisito).',
'La tabella elenca <strong>solo i requisiti applicabili alla tua classe</strong> (importante &rarr; Allegato 1, essenziale &rarr; Allegato 2 della Determinazione ACN 164179/2025). Se l\'elenco e\' lungo, il riquadro <strong>scorre verticalmente</strong> con l\'intestazione fissa.',
'La seconda tabella, <strong>Rischi aggiuntivi (approccio multirischio / all-hazard)</strong>, permette ai <strong>profili amministrativi</strong> di aggiungere rischi non coperti da un singolo requisito (minacce fisiche, catena di fornitura, eventi naturali): copre l\'approccio multirischio richiesto dall\'art. 24 D.Lgs. 138/2024.'
]
},
{
+8 -2
View File
@@ -171,8 +171,14 @@ const I18n = (function () {
'risks.avoid': { it: 'Evitare', en: 'Avoid' },
'risks.derived_heading': { it: 'Rischi dai requisiti del framework di sicurezza', en: 'Risks from security framework requirements' },
'risks.derived_code_legend': { it: '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.', en: 'RSK-‹measure›Com.‹n› is an internal NIS2 Agile code; ‹measure› (e.g. GV.OC-04) is the National Framework measure code referenced by ACN Determination 164179/2025. Read-only list.' },
'risks.col_code': { it: 'Codice', en: 'Code' },
'risks.col_risk': { it: 'Rischio', en: 'Risk' },
'risks.col_code': { it: 'Codice rischio', en: 'Risk code' },
'risks.col_risk': { it: 'Descrizione rischio', en: 'Risk description' },
'risks.grp_inherent': { it: 'VALUTAZIONE RISCHIO INERENTE', en: 'INHERENT RISK ASSESSMENT' },
'risks.grp_residual': { it: 'VALUTAZIONE RISCHIO RESIDUO', en: 'RESIDUAL RISK ASSESSMENT' },
'risks.col_alert': { it: 'Alert', en: 'Alert' },
'risks.col_inherent': { it: 'Rischio Inerente', en: 'Inherent risk' },
'risks.col_res_outcome': { it: 'Esito residuo', en: 'Residual outcome' },
'risks.col_residual': { it: 'Rischio Residuo', en: 'Residual risk' },
'risks.col_measure': { it: 'Misura', en: 'Measure' },
'risks.col_conformity': { it: 'Conformità', en: 'Conformity' },
'risks.col_evaluation': { it: 'Valutazione', en: 'Evaluation' },
+3 -3
View File
@@ -175,12 +175,12 @@
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → kb.js -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script src="/js/kb.js"></script>
<script>
// Gate auth + chrome come le altre pagine V3.
+1 -1
View File
@@ -515,7 +515,7 @@ body { background: var(--bg-main); }
<div id="toast"></div>
<script src="js/api.js?v=20260729b"></script>
<script src="js/api.js?v=20260730a"></script>
<script>
// ══════════════════════════════════════════════════════
// CONFIG
+1 -1
View File
@@ -94,7 +94,7 @@
</script>
<!-- Stessa logica della login attuale: ZERO modifiche backend -->
<script src="js/api.js?v=20260729b"></script>
<script src="js/api.js?v=20260730a"></script>
<script src="js/common.js?v=20260729b"></script>
<script>
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
+3 -3
View File
@@ -147,12 +147,12 @@
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
'use strict';
/*
+3 -3
View File
@@ -175,12 +175,12 @@
</div>
<!-- Scripts V3 (ordine ESATTO come dashboard.html) -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
'use strict';
/*
+3 -3
View File
@@ -137,12 +137,12 @@
</main>
<!-- Scripts V3 (ordine ESATTO come dashboard.html) -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
// ── Auth + chrome + i18n + help ──────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+1 -1
View File
@@ -494,7 +494,7 @@
</script>
<!-- Stessa logica della onboarding.html: ZERO modifiche backend -->
<script src="js/api.js?v=20260729b"></script>
<script src="js/api.js?v=20260730a"></script>
<script src="js/common.js?v=20260729b"></script>
<script>
// ── Auth check ──────────────────────────────────────────────────
+3 -3
View File
@@ -151,12 +151,12 @@
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → organigramma.js.
organigramma.js si auto-inizializza su DOMContentLoaded (checkAuth/loadSidebar/I18n/Help). -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script src="/js/organigramma.js?v=20260627"></script>
<script>
// Render icone Lucide (chrome + crumb + bottoni). organigramma.js fa render albero
+3 -3
View File
@@ -340,12 +340,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=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
// ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+3 -3
View File
@@ -141,12 +141,12 @@
</main>
<!-- Scripts V3. raci.js si auto-inizializza su DOMContentLoaded (checkAuth/loadSidebar/I18n/Help). -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script src="/js/raci.js?v=20260627"></script>
<script>
function v3Icons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } }
+1 -1
View File
@@ -268,7 +268,7 @@
</script>
<!-- Stessa logica della register.html: ZERO modifiche backend -->
<script src="js/api.js?v=20260729b"></script>
<script src="js/api.js?v=20260730a"></script>
<script src="js/common.js?v=20260729b"></script>
<script>
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
+3 -3
View File
@@ -232,12 +232,12 @@
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
// ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+3 -3
View File
@@ -157,12 +157,12 @@
</div>
<!-- Scripts V3 (ordine ESATTO come dashboard.html) -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
'use strict';
/*
+243 -62
View File
@@ -306,10 +306,44 @@
}
#view-derived .table-container.scroll-box thead th {
position: sticky;
top: 0;
background: var(--gray-50);
z-index: 2;
}
/* Tabella 12 colonne (spec Simon): 2 righe di intestazione, gruppi inerente/residuo */
table.derived-12 thead tr:first-child th { top: 0; }
table.derived-12 thead tr:nth-child(2) th { top: 30px; }
table.derived-12 th, table.derived-12 td {
font-size: 0.8rem; padding: 6px 8px; text-align: center; vertical-align: middle;
}
table.derived-12 th.grp {
font-size: 0.66rem; letter-spacing: 0.04em; font-weight: 700; color: #fff;
}
table.derived-12 th.grp-inh { background: #b45309; }
table.derived-12 th.grp-res { background: #0e7490; }
table.derived-12 td.cell-code, table.derived-12 td.cell-desc { text-align: left; }
.rk-eval { display:inline-flex; align-items:center; justify-content:center; min-width:26px; height:22px;
padding:0 6px; border-radius:6px; font-weight:800; font-size:0.78rem; color:#fff; }
.rk-low { background:#16a34a; } .rk-med { background:#d97706; }
.rk-high { background:#ea580c; } .rk-crit { background:#dc2626; }
.rk-sel { width:52px; padding:2px 4px; font-size:0.78rem; border:1px solid var(--gray-300);
border-radius:6px; background:#fff; text-align:center; }
.rk-sel:focus { border-color: var(--primary); outline:none; }
.alert-btn { border:0; border-radius:16px; padding:4px 10px; font-size:0.7rem; font-weight:800;
letter-spacing:.02em; cursor:pointer; white-space:nowrap; }
.alert-pri { background:#dc2626; color:#fff; }
.alert-cri { background:#f87171; color:#fff; }
.alert-nec { background:#f59e0b; color:#fff; }
.alert-sug { background:#16a34a; color:#fff; }
.flag-riv { display:inline-block; padding:3px 9px; border-radius:14px; font-size:0.7rem; font-weight:800;
background:#fee2e2; color:#b91c1c; }
.flag-adeg { display:inline-block; padding:3px 9px; border-radius:14px; font-size:0.7rem; font-weight:800;
background:#dcfce7; color:#15803d; }
.rk-valutato { font-size:0.72rem; font-weight:700; color:#15803d; }
.rk-empty { color: var(--gray-300); }
.act-row { display:flex; align-items:flex-start; gap:10px; padding:10px 4px; border-bottom:1px solid var(--gray-100); }
.act-row .act-when { font-size:0.72rem; color:var(--gray-500); white-space:nowrap; min-width:74px; }
.act-row .act-desc { flex:1; font-size:0.82rem; color:var(--gray-800); }
.act-row .act-st { font-size:0.68rem; font-weight:700; text-transform:uppercase; color:var(--gray-500); }
</style>
<!-- PWA:start -->
<link rel="manifest" href="/manifest.webmanifest">
@@ -367,23 +401,30 @@
</p>
<div class="card">
<div class="table-container scroll-box">
<table>
<table class="derived-12">
<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" rowspan="2" data-i18n="risks.col_code">Codice rischio</th>
<th scope="col" rowspan="2" data-i18n="risks.col_risk">Descrizione rischio</th>
<th scope="colgroup" colspan="3" class="grp grp-inh" data-i18n="risks.grp_inherent">VALUTAZIONE RISCHIO INERENTE</th>
<th scope="col" rowspan="2" data-i18n="risks.col_alert">Alert</th>
<th scope="col" rowspan="2" data-i18n="risks.col_inherent">Rischio Inerente</th>
<th scope="colgroup" colspan="3" class="grp grp-res" data-i18n="risks.grp_residual">VALUTAZIONE RISCHIO RESIDUO</th>
<th scope="col" rowspan="2" data-i18n="risks.col_res_outcome">Esito residuo</th>
<th scope="col" rowspan="2" data-i18n="risks.col_residual">Rischio Residuo</th>
</tr>
<tr>
<th scope="col" data-i18n="risks.col_probability">Probabilità</th>
<th scope="col" data-i18n="risks.col_impact">Impatto</th>
<th scope="col" data-i18n="risks.col_evaluation">Valutazione</th>
<th scope="col" data-i18n="risks.col_probability">Probabilità</th>
<th scope="col" data-i18n="risks.col_impact">Impatto</th>
<th scope="col" data-i18n="risks.col_risk_level">Rischio (P×I)</th>
<th scope="col" data-i18n="risks.col_evaluation">Valutazione</th>
<th scope="col" id="derived-th-actions" data-i18n="risks.col_actions" style="display:none;">Azioni</th>
</tr>
</thead>
<tbody id="derived-table-body">
<tr>
<td colspan="5">
<td colspan="12">
<div class="spinner" style="margin:40px auto;"></div>
</td>
</tr>
@@ -499,12 +540,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=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
// ── Auth & Init ──────────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
@@ -634,16 +675,44 @@
return 'score-low';
}
// ── Livello del punteggio 1-25 (classe colore del riquadro "Valutazione") ──
function evalClass(p) {
if (p >= 15) return 'rk-crit';
if (p >= 9) return 'rk-high';
if (p >= 3) return 'rk-med';
return 'rk-low';
}
// ── Banda ALERT sul punteggio INERENTE (prodotto P×I). Eccezione #3: la banda
// 13-14 (scoperta nella spec) è inclusa in "Critica" → 9-14 Critica, 15-25 Prioritaria. ──
function alertBand(p) {
if (p == null) return null;
if (p >= 15) return { cls: 'alert-pri', label: 'Azione Prioritaria' };
if (p >= 9) return { cls: 'alert-cri', label: 'Azione Critica' };
if (p >= 3) return { cls: 'alert-nec', label: 'Azione Necessaria' };
return { cls: 'alert-sug', label: 'Azione Suggerita' };
}
// ── Esito del RESIDUO (col.11). Eccezione #1: NON è un verdetto di conformità,
// è un flag di rivalutazione. ≥9 → "Rivalutare"; 1-8 → "Adeguato". ──
function residualOutcome(p) {
if (p == null) return null;
return p >= 9
? { cls: 'flag-riv', label: 'Rivalutare' }
: { cls: 'flag-adeg', label: 'Adeguato' };
}
function scoreSel(id, val, editable) {
if (!editable) return (val >= 1 && val <= 5) ? (val + '/5') : '<span class="rk-empty">—</span>';
let o = `<option value="">—</option>`;
for (let v = 1; v <= 5; v++) o += `<option value="${v}" ${v == val ? 'selected' : ''}>${v}</option>`;
return `<select class="rk-sel" id="${id}" onchange="onScoreChange(${id.split('-')[1]})">${o}</select>`;
}
function renderDerivedTable(rows) {
const tbody = document.getElementById('derived-table-body');
const colspan = 8 + (canEvaluate ? 1 : 0);
// Ticket #501/2: mostra SOLO i requisiti applicabili alla classe del soggetto
// (importante → Allegato 1, essenziale → Allegato 2 della Det. ACN 164179/2025).
// Le righe non applicabili non vengono più elencate (prima erano solo attenuate).
// Ticket #501/2: mostra SOLO i requisiti applicabili alla classe del soggetto.
rows = (rows || []).filter(isDerivedApplicable);
if (!rows || rows.length === 0) {
tbody.innerHTML = `
<tr><td colspan="${colspan}">
<tr><td colspan="12">
<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>
@@ -655,66 +724,178 @@
let html = '';
rows.forEach(r => {
const applicable = isDerivedApplicable(r);
const st = DERIVED_STATUS_BADGE[r.stato] || DERIVED_STATUS_BADGE.da_valutare;
const reqId = r.requisito_id;
const editable = canEvaluate && r.stato !== 'non_applicabile';
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();
const riskDescr = r.risk_descr || r.requisito_descr || '';
const st = DERIVED_STATUS_BADGE[r.stato] || DERIVED_STATUS_BADGE.da_valutare;
const riskTooltip = `Rischio di non conformità legato al 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>`;
// INERENTE
const iL = (r.likelihood >= 1 && r.likelihood <= 5) ? parseInt(r.likelihood) : null;
const iI = (r.impact >= 1 && r.impact <= 5) ? parseInt(r.impact) : null;
const iP = (iL != null && iI != null) ? iL * iI : null;
// RESIDUO
const rL = (r.residual_likelihood >= 1 && r.residual_likelihood <= 5) ? parseInt(r.residual_likelihood) : null;
const rI = (r.residual_impact >= 1 && r.residual_impact <= 5) ? parseInt(r.residual_impact) : null;
const rP = (rL != null && rI != null) ? rL * rI : null;
// Probabilità / Impatto / Rischio: solo per righe applicabili e non "non applicabili".
let probCell = '<td>—</td>', impCell = '<td>—</td>', scoreCell = '<td>—</td>';
if (applicable && r.stato !== 'non_applicabile') {
const l = (r.likelihood >= 1 && r.likelihood <= 5) ? parseInt(r.likelihood) : null;
const i = (r.impact >= 1 && r.impact <= 5) ? parseInt(r.impact) : null;
probCell = `<td>${l != null ? l + '/5' : '—'}</td>`;
impCell = `<td>${i != null ? i + '/5' : '—'}</td>`;
if (r.score != null && r.score !== '' && !isNaN(r.score)) {
const sc = parseInt(r.score);
scoreCell = `<td><span class="risk-score ${getDerivedScoreClass(sc)}" title="P${l != null ? l : '?'} × I${i != null ? i : '?'} = ${sc}">${sc}</span></td>`;
}
}
const iEval = iP != null ? `<span class="rk-eval ${evalClass(iP)}" id="iv-${reqId}">${iP}</span>` : `<span class="rk-empty" id="iv-${reqId}">—</span>`;
const rEval = rP != null ? `<span class="rk-eval ${evalClass(rP)}" id="rv-${reqId}">${rP}</span>` : `<span class="rk-empty" id="rv-${reqId}">—</span>`;
// Colonna Azioni (solo per i ruoli che possono valutare).
let actionsCell = '';
if (canEvaluate) {
actionsCell = applicable
? `<td><button class="btn btn-ghost btn-sm" onclick="openEvaluateModal(${r.requisito_id})" title="Valuta rischio"><i data-lucide="sliders-horizontal" class="ic"></i> Valuta</button></td>`
: '<td><span class="text-muted">—</span></td>';
}
const ab = alertBand(iP);
const alertCell = ab
? `<button class="alert-btn ${ab.cls}" onclick="openActionsPopup(${reqId})" title="Vedi/crea azioni">${ab.label}</button>`
: `<span class="rk-empty" id="al-${reqId}">—</span>`;
const inhOutcome = iP != null ? '<span class="rk-valutato">Valutato</span>' : '<span class="rk-empty">—</span>';
const ro = residualOutcome(rP);
const resOutcomeCell = ro ? `<span class="${ro.cls}">${ro.label}</span>` : '<span class="rk-empty">—</span>';
// Col.12 — Rischio Residuo: "Valutato" + stato requisito ESPLICITO (eccezione #1: non automatico).
const resCell = rP != null
? `<span class="rk-valutato">Valutato</span>${editable ? `<div style="margin-top:3px;"><a href="javascript:void(0)" onclick="openStateModal(${reqId})" style="font-size:0.68rem;color:var(--primary);">stato: ${escapeHtml(st.label)}</a></div>` : `<div style="font-size:0.68rem;color:var(--gray-500);margin-top:3px;">${escapeHtml(st.label)}</div>`}`
: '<span class="rk-empty">—</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>
<tr>
<td class="cell-code">
<a href="misure-requisiti.html?req=${reqId}" title="Apri in Misure e Requisiti"><code style="font-size:0.75rem;color:var(--primary);">${escapeHtml(r.risk_code || '-')}</code></a>
<div style="font-size:0.66rem;color:var(--gray-500);margin-top:2px;">${escapeHtml(measureCode)} ${escapeHtml(reqCode)}</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 class="cell-desc" title="${escapeHtml(riskTooltip)}">
<div style="font-size:0.64rem;text-transform:uppercase;letter-spacing:0.04em;font-weight:600;color:var(--gray-500);">Rischio di non conformità</div>
<div style="font-size:0.82rem;color:var(--gray-800);">${escapeHtml(riskDescr)}</div>
</td>
<td>${conformityCell}</td>
${probCell}
${impCell}
${scoreCell}
<td>${evalVal}</td>
${actionsCell}
<td>${scoreSel('il-' + reqId, iL, editable)}</td>
<td>${scoreSel('ii-' + reqId, iI, editable)}</td>
<td>${iEval}</td>
<td>${alertCell}</td>
<td>${inhOutcome}</td>
<td>${scoreSel('rl-' + reqId, rL, editable)}</td>
<td>${scoreSel('ri-' + reqId, rI, editable)}</td>
<td>${rEval}</td>
<td>${resOutcomeCell}</td>
<td>${resCell}</td>
</tr>`;
});
tbody.innerHTML = html;
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
}
// ── Editing inline: legge i 4 punteggi della riga, salva l'intera valutazione, ri-render. ──
async function onScoreChange(reqId) {
const r = (derivedData || []).find(x => String(x.requisito_id) === String(reqId));
if (!r) return;
const rd = id => { const el = document.getElementById(id); if (!el) return null; const v = el.value; return v === '' ? null : parseInt(v); };
r.likelihood = rd('il-' + reqId);
r.impact = rd('ii-' + reqId);
r.score = (r.likelihood && r.impact) ? r.likelihood * r.impact : null;
r.residual_likelihood = rd('rl-' + reqId);
r.residual_impact = rd('ri-' + reqId);
r.residual_score = (r.residual_likelihood && r.residual_impact) ? r.residual_likelihood * r.residual_impact : null;
renderDerivedTable(derivedData);
loadMatrix();
try {
await api.frameworkSetState({
requisito_id: parseInt(reqId),
stato: r.stato || 'da_valutare',
valutazione_rischio: r.valutazione_rischio || '',
likelihood: r.likelihood, impact: r.impact,
residual_likelihood: r.residual_likelihood, residual_impact: r.residual_impact,
});
} catch (e) {
showNotification(e.message || 'Errore nel salvataggio', 'error');
}
}
// ── Popup Azioni (col.6 Alert): elenco azioni passate + "crea azione" ──
async function openActionsPopup(reqId) {
const r = (derivedData || []).find(x => String(x.requisito_id) === String(reqId));
if (!r) return;
let actions = [];
try {
const res = await api.requisitoActions(reqId);
if (res && res.success && res.data) actions = res.data.actions || [];
} catch (e) { /* mostra comunque il popup */ }
const today = new Date().toLocaleDateString('it-IT');
let list = `
<div class="act-row" style="background:var(--gray-50);border-radius:8px;">
<div class="act-when"><b>${today}</b></div>
<div class="act-desc"><input type="text" class="form-input" id="new-act-descr" placeholder="Descrivi la nuova azione…" style="width:100%;"></div>
<button class="btn btn-primary btn-sm" onclick="createReqAction(${reqId})">Crea azione</button>
</div>`;
if (actions.length) {
list += actions.map(a => {
const when = a.created_at ? new Date(a.created_at.replace(' ', 'T')).toLocaleDateString('it-IT') : '—';
return `<div class="act-row"><div class="act-when">${when}</div><div class="act-desc">${escapeHtml(a.description || '')}</div><div class="act-st">${escapeHtml(a.status || '')}</div></div>`;
}).join('');
} else {
list += `<div style="padding:14px 4px;color:var(--gray-500);font-size:0.82rem;">Nessuna azione precedente su questo rischio.</div>`;
}
const content = `
<div style="padding:10px 12px;background:var(--gray-50);border:1px solid var(--gray-200);border-radius:8px;margin-bottom:14px;">
<code style="font-size:0.78rem;color:var(--primary);">${escapeHtml(r.risk_code || '-')}</code>
<div style="font-size:0.82rem;color:var(--gray-800);margin-top:3px;">${escapeHtml(r.risk_descr || r.requisito_descr || '')}</div>
</div>
<p class="text-muted" style="font-size:0.74rem;margin:0 0 10px;">La soglia di trattamento è una scelta metodologica dell'organizzazione (rif. ISO/IEC 27001 §6.1.2), non un obbligo di legge.</p>
${list}`;
showModal('Azioni sul rischio', content, {
size: 'lg',
footer: `<button class="btn btn-secondary" onclick="closeModal()">Chiudi</button>`
});
}
async function createReqAction(reqId) {
const el = document.getElementById('new-act-descr');
const descr = (el && el.value || '').trim();
if (!descr) { showNotification('Inserisci una descrizione', 'error'); return; }
const r = (derivedData || []).find(x => String(x.requisito_id) === String(reqId));
try {
const res = await api.createRequisitoAction({ requisito_id: parseInt(reqId), description: descr, risk_code: r ? r.risk_code : null });
if (res && res.success) {
showNotification('Azione creata', 'success');
openActionsPopup(reqId);
} else {
showNotification((res && res.message) || 'Errore', 'error');
}
} catch (e) { showNotification(e.message || 'Errore', 'error'); }
}
// ── Aggiorna ESPLICITAMENTE lo stato di conformità del requisito (eccezione #1) ──
function openStateModal(reqId) {
const r = (derivedData || []).find(x => String(x.requisito_id) === String(reqId));
if (!r || !canEvaluate) return;
const opts = DERIVED_STATUS_OPTIONS.map(([v, l]) =>
`<option value="${v}" ${v === (r.stato || 'da_valutare') ? 'selected' : ''}>${escapeHtml(l)}</option>`).join('');
const content = `
<p class="text-muted" style="font-size:0.8rem;line-height:1.5;">Lo stato di <b>conformità</b> dipende dall'<b>attuazione della misura e dalle evidenze</b>, non dal punteggio di rischio. Aggiornalo qui in modo esplicito.</p>
<div class="form-group"><label class="form-label">Stato del requisito</label><select class="form-select" id="st-sel">${opts}</select></div>`;
showModal('Stato del requisito', content, {
size: 'md',
footer: `<button class="btn btn-secondary" onclick="closeModal()">Annulla</button><button class="btn btn-primary" onclick="saveState(${reqId})">Salva stato</button>`
});
}
async function saveState(reqId) {
const r = (derivedData || []).find(x => String(x.requisito_id) === String(reqId));
if (!r) return;
const stato = document.getElementById('st-sel').value;
try {
await api.frameworkSetState({
requisito_id: parseInt(reqId), stato: stato,
valutazione_rischio: r.valutazione_rischio || '',
likelihood: r.likelihood, impact: r.impact,
residual_likelihood: r.residual_likelihood, residual_impact: r.residual_impact,
});
r.stato = stato;
closeModal();
showNotification('Stato aggiornato', 'success');
renderDerivedTable(derivedData);
} catch (e) { showNotification(e.message || 'Errore', 'error'); }
}
// ── Modale "Valuta" (opzione B) — valutazione rischio-da-requisito ──
const DERIVED_STATUS_OPTIONS = [
['da_valutare', 'Da valutare'],
+3 -3
View File
@@ -735,12 +735,12 @@
<!-- Scripts V3: api.js (client REST, prima) → common.js (helper+FAB reali) →
Lucide self-host → topnav-v3.js (chrome, ridefinisce loadSidebar) →
i18n → help → init pagina (inline, invariato). -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+3 -3
View File
@@ -215,12 +215,12 @@
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
'use strict';
/*
+3 -3
View File
@@ -210,12 +210,12 @@
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
'use strict';
/*
+3 -3
View File
@@ -542,12 +542,12 @@
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
// ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+3 -3
View File
@@ -190,12 +190,12 @@
<!-- Scripts V3: api.js → common.js (helper+FAB reali+showModal/showNotification) →
Lucide self-host → topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help.
La logica della pagina e' inline (nessun training.js): preservata invariata. -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
// ── Auth & Init (ordine V3) ─────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+3 -3
View File
@@ -258,12 +258,12 @@
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar();
+3 -3
View File
@@ -143,12 +143,12 @@
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina. -->
<script src="/js/api.js?v=20260729b"></script>
<script src="/js/api.js?v=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260729"></script>
<script src="/js/help.js?v=20260729b"></script>
<script src="/js/i18n.js?v=20260730a"></script>
<script src="/js/help.js?v=20260730a"></script>
<script>
// ── Auth + chrome + i18n + help ──────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');