[FEAT] UI: guida online, landing EN, mobile-conversion, ai-assistant, bug-reporter + help/i18n

- public/guida.html, index-en.html, service-continuity.html
- public/js/ai-assistant.js, bug-reporter.js (FAB supporto)
- public/mobile-conversion.css/js
- index.html, common.js, help.js, risks.html: aggiornamenti UI/help

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile 2026-05-29 15:42:00 +02:00
parent c0bf7b6c15
commit 1d934e4e63
11 changed files with 4227 additions and 19 deletions

775
public/guida.html Normal file
View File

@ -0,0 +1,775 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Guida all'uso - NIS2 Agile</title>
<link rel="stylesheet" href="css/style.css">
<style>
/* Stili specifici per la guida — non interferiscono col resto */
.guide-wrap { max-width: 920px; margin: 0 auto; }
.guide-toc {
position: sticky; top: 16px;
background: var(--surface, #fff);
border: 1px solid var(--border, #e5e7eb);
border-radius: 10px; padding: 16px 20px;
margin-bottom: 24px;
}
.guide-toc h4 { margin: 0 0 10px; font-size: 0.9rem; color: var(--text-muted, #6b7280); text-transform: uppercase; letter-spacing: 0.05em; }
.guide-toc ul { list-style: none; padding: 0; margin: 0; columns: 2; column-gap: 24px; }
.guide-toc li { padding: 4px 0; }
.guide-toc a { color: var(--primary, #1e40af); text-decoration: none; font-size: 0.93rem; }
.guide-toc a:hover { text-decoration: underline; }
.guide-section { margin: 40px 0; padding-top: 16px; scroll-margin-top: 16px; }
.guide-section h2 {
color: var(--primary, #1e40af);
border-bottom: 2px solid var(--primary, #1e40af);
padding-bottom: 8px; margin-bottom: 20px;
display: flex; align-items: center; gap: 12px;
}
.guide-section h2 .num {
background: var(--primary, #1e40af); color: #fff;
width: 32px; height: 32px; border-radius: 50%;
display: inline-flex; align-items: center; justify-content: center;
font-size: 0.9rem; font-weight: 700;
}
.guide-section h3 { color: #374151; margin-top: 24px; }
.plain-box {
background: #eff6ff; border-left: 4px solid #1e40af;
padding: 14px 18px; border-radius: 6px; margin: 14px 0;
}
.plain-box strong { color: #1e40af; }
.example-box {
background: #fef3c7; border-left: 4px solid #f59e0b;
padding: 14px 18px; border-radius: 6px; margin: 14px 0;
}
.example-box strong { color: #92400e; }
.norm-box {
background: #f3f4f6; border-left: 4px solid #6b7280;
padding: 14px 18px; border-radius: 6px; margin: 14px 0;
font-size: 0.93rem;
}
.norm-box .article-tag {
display: inline-block; background: #1e40af; color: #fff;
padding: 2px 8px; border-radius: 4px; font-size: 0.78rem;
font-weight: 600; margin-right: 8px;
}
.step-list { counter-reset: step; list-style: none; padding-left: 0; }
.step-list li {
counter-increment: step; padding: 12px 12px 12px 56px;
position: relative; margin: 8px 0;
background: #f9fafb; border-radius: 6px;
}
.step-list li::before {
content: counter(step);
position: absolute; left: 14px; top: 12px;
background: var(--primary, #1e40af); color: #fff;
width: 30px; height: 30px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-weight: 700;
}
.glossario dt {
font-weight: 700; color: var(--primary, #1e40af);
margin-top: 14px; font-size: 1.02rem;
}
.glossario dd { margin-left: 0; margin-bottom: 6px; color: #374151; }
.glossario .acro { font-family: 'Courier New', monospace; background: #e0e7ff; padding: 2px 6px; border-radius: 3px; }
.pillar-card {
border: 1px solid #e5e7eb; border-radius: 8px;
padding: 16px; margin: 10px 0;
background: #fff;
}
.pillar-card h4 {
margin: 0 0 8px; color: var(--primary, #1e40af);
display: flex; align-items: center; gap: 8px;
}
.pillar-card .pillar-num {
background: var(--primary, #1e40af); color: #fff;
padding: 2px 8px; border-radius: 4px; font-size: 0.8rem;
}
@media (max-width: 768px) {
.guide-toc ul { columns: 1; }
}
</style>
</head>
<body>
<div class="app-layout">
<aside class="sidebar" id="sidebar"></aside>
<main class="main-content">
<header class="content-header">
<h2>Guida all'uso di NIS2 Agile</h2>
<div class="content-header-actions">
<span class="text-muted">Per chi inizia da zero</span>
</div>
</header>
<div class="content-body">
<div class="guide-wrap">
<!-- Intro -->
<div class="card">
<div class="card-body">
<p style="font-size:1.05rem; line-height:1.7; margin:0;">
Benvenuto. Questa guida ti accompagna passo passo nell'uso della piattaforma <strong>NIS2 Agile</strong>,
spiegando con parole semplici cosa devi fare e perché — senza dare per scontato che tu sia un esperto
di cybersecurity. Troverai per ogni argomento <strong style="color:#1e40af;">la spiegazione in parole semplici</strong>,
un <strong style="color:#92400e;">esempio pratico</strong> e <strong style="color:#6b7280;">cosa dice la norma</strong>.
</p>
</div>
</div>
<!-- TOC -->
<nav class="guide-toc" aria-label="Indice">
<h4>Indice</h4>
<ul>
<li><a href="#cap-1">1. Cos'è la NIS2 (5 minuti)</a></li>
<li><a href="#cap-2">2. La tua azienda è "in scope"?</a></li>
<li><a href="#cap-3">3. Cosa fa la piattaforma</a></li>
<li><a href="#cap-4">4. Il percorso tipico</a></li>
<li><a href="#cap-5">5. Gap Analysis (Art. 21)</a></li>
<li><a href="#cap-6">6. Gestione dei Rischi</a></li>
<li><a href="#cap-7">7. Incidenti (Art. 23)</a></li>
<li><a href="#cap-8">8. Policy e procedure</a></li>
<li><a href="#cap-9">9. Fornitori (Supply Chain)</a></li>
<li><a href="#cap-10">10. Formazione (Art. 20)</a></li>
<li><a href="#cap-11">11. Asset</a></li>
<li><a href="#cap-12">12. Audit &amp; Report</a></li>
<li><a href="#cap-13">13. Segnalazioni interne</a></li>
<li><a href="#cap-14">14. AI: come usarla</a></li>
<li><a href="#cap-15">15. Glossario</a></li>
</ul>
</nav>
<!-- Cap 1 -->
<section id="cap-1" class="guide-section">
<h2><span class="num">1</span> Cos'è la NIS2 (in 5 minuti)</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> NIS2 è una legge europea (Direttiva UE 2022/2555, recepita in Italia
con il D.Lgs. 138/2024) che obbliga molte aziende a proteggere bene i propri sistemi informatici.
Lo scopo è evitare che attacchi hacker, fughe di dati o blocchi dei sistemi danneggino cittadini,
servizi essenziali (ospedali, energia, acqua, trasporti) e l'economia europea.
</div>
<p>NIS2 stabilisce quattro grandi obblighi:</p>
<ol>
<li><strong>Misurare il rischio cyber</strong> della propria azienda e migliorare le difese (Art. 21).</li>
<li><strong>Segnalare gli incidenti</strong> gravi alle autorità entro tempi precisi (Art. 23).</li>
<li><strong>Formare il personale</strong>, soprattutto i dirigenti (Art. 20).</li>
<li><strong>Controllare i fornitori</strong> che hanno accesso ai propri sistemi (Art. 21, lettera d).</li>
</ol>
<div class="example-box">
<strong>Esempio reale.</strong> Una clinica privata con 250 dipendenti è "in scope" NIS2 perché
opera nel settore sanitario (settore essenziale). Se subisce un ransomware che blocca le cartelle cliniche
per 8 ore, deve notificarlo al CSIRT Italia entro 24 ore con una prima segnalazione e poi entro 72 ore
con i dettagli completi.
</div>
<div class="norm-box">
<span class="article-tag">Considerando UE</span>
La direttiva sostituisce la "NIS1" del 2016 ampliando settori coinvolti, inasprendo le sanzioni
(fino al 2% del fatturato globale o 10 milioni €) e introducendo la responsabilità diretta degli
organi di vertice (amministratori) sulla cybersecurity.
</div>
</section>
<!-- Cap 2 -->
<section id="cap-2" class="guide-section">
<h2><span class="num">2</span> La tua azienda è "in scope"?</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> "In scope" vuol dire che la legge si applica a te. Dipende da
<em>cosa fai</em> (settore) e <em>quanto sei grande</em> (dipendenti e fatturato).
</div>
<h3>I settori coinvolti</h3>
<p>Sono divisi in due gruppi:</p>
<div class="pillar-card">
<h4>Settori <strong style="color:#dc2626;">essenziali</strong> (controlli più stringenti)</h4>
<p>Energia, trasporti, banche, finanza, sanità, acqua potabile e reflue, infrastrutture digitali
(data center, DNS, TLD), Pubblica Amministrazione, spazio.</p>
</div>
<div class="pillar-card">
<h4>Settori <strong style="color:#f59e0b;">importanti</strong> (controlli normali)</h4>
<p>Servizi postali, gestione rifiuti, chimica, alimentare, manifattura (apparecchi medici, computer,
veicoli, ecc.), provider digitali (motori di ricerca, social, marketplace), ricerca.</p>
</div>
<h3>Le soglie dimensionali</h3>
<ul>
<li><strong>Media impresa</strong> = 50249 dipendenti <em>oppure</em> fatturato tra 10 e 50 milioni €.</li>
<li><strong>Grande impresa</strong> = ≥250 dipendenti <em>oppure</em> fatturato &gt;50 milioni €.</li>
</ul>
<p>In generale: medie e grandi imprese nei settori sopra elencati sono in scope. Le piccole sono in scope
solo in casi particolari (es. fornitori di servizi DNS, TLD, registrar).</p>
<div class="example-box">
<strong>Esempio pratico.</strong> "Aurora Sanità S.p.A." con 480 dipendenti e 92 milioni € di fatturato,
settore sanitario → <strong>essenziale + grande</strong> → pienamente in scope. Deve registrarsi sul portale
ACN (Agenzia per la Cybersicurezza Nazionale) entro le scadenze.
</div>
<div class="norm-box">
<span class="article-tag">Art. 2 + Allegati I e II</span>
L'ambito di applicazione è definito dagli articoli 2 della Direttiva e specificato negli Allegati I (settori
essenziali) e II (settori importanti) del D.Lgs. 138/2024.
</div>
</section>
<!-- Cap 3 -->
<section id="cap-3" class="guide-section">
<h2><span class="num">3</span> Cosa fa la piattaforma NIS2 Agile</h2>
<p>La piattaforma ti aiuta a fare <strong>tutto quello che la NIS2 chiede</strong>, in modo organizzato e
documentabile. Non sostituisce il giudizio di un consulente o un CISO, ma ti dà gli strumenti per:</p>
<ul>
<li>Capire <strong>quanto sei conforme</strong> oggi (Gap Analysis con 80 domande).</li>
<li>Tenere un <strong>registro dei rischi</strong> aggiornato.</li>
<li>Gestire gli <strong>incidenti</strong> con i moduli di notifica già pronti per il CSIRT.</li>
<li>Generare <strong>policy</strong> di sicurezza usando l'AI.</li>
<li>Monitorare i <strong>fornitori critici</strong>.</li>
<li>Pianificare la <strong>formazione</strong> dei dipendenti.</li>
<li>Inventariare gli <strong>asset</strong> critici.</li>
<li>Estrarre <strong>report</strong> per audit interni o per le autorità.</li>
</ul>
</section>
<!-- Cap 4 -->
<section id="cap-4" class="guide-section">
<h2><span class="num">4</span> Il percorso tipico (cosa fare per primo)</h2>
<p>Se è la tua prima volta, segui questi passi in ordine:</p>
<ol class="step-list">
<li><strong>Completa l'Onboarding</strong> — inserisci i dati aziendali (puoi caricare la visura
e l'AI estrae i dati automaticamente). Classifica la tua azienda come essenziale/importante.</li>
<li><strong>Fai un primo Assessment (Gap Analysis)</strong> — rispondi alle 80 domande, anche se
in più sessioni. Otterrai un punteggio di maturità complessiva.</li>
<li><strong>Crea il Risk Register</strong> — parti dai rischi più ovvi (ransomware, phishing,
guasto sistemi). L'AI può suggerirti rischi tipici del tuo settore.</li>
<li><strong>Inserisci gli asset critici</strong> — i sistemi/dati senza i quali l'azienda si ferma.</li>
<li><strong>Mappa i fornitori critici</strong> — quelli con accesso ai tuoi sistemi o dati.</li>
<li><strong>Genera/approva le policy fondamentali</strong> — usa l'AI per le bozze, poi rivedile.</li>
<li><strong>Pianifica la formazione</strong> — soprattutto per i dirigenti (obbligo Art. 20).</li>
<li><strong>Quando arriva un incidente</strong> — usalo dal modulo Incidenti per gestire la notifica
24h/72h/30d.</li>
<li><strong>Genera il report esecutivo</strong> dalla sezione Report — utile per il board.</li>
</ol>
<div class="example-box">
<strong>Tempi indicativi.</strong> Un primo ciclo "decente" si fa in 68 settimane di lavoro
spalmate, con 2 persone (un IT e un compliance). Non puntare alla perfezione subito: meglio
coprire <em>tutte</em> le 10 categorie al 50% che 3 al 100% e 7 a zero.
</div>
</section>
<!-- Cap 5 -->
<section id="cap-5" class="guide-section">
<h2><span class="num">5</span> Gap Analysis — le 10 misure dell'Art. 21</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> L'Art. 21 elenca 10 famiglie di "cose che devi avere".
La Gap Analysis ti fa domande per ognuna e calcola quanto sei attrezzato. Non serve essere
perfetti — serve <strong>sapere dove sei messo male</strong> e <strong>aver iniziato a migliorare</strong>.
</div>
<p>Le 10 misure (semplificate):</p>
<div class="pillar-card">
<h4><span class="pillar-num">a)</span> Politiche di analisi del rischio</h4>
<p><em>Hai scritto come la tua azienda gestisce il rischio cyber?</em> Ti serve almeno una policy
approvata dall'amministratore delegato che descriva il metodo (es. ISO 27005).</p>
</div>
<div class="pillar-card">
<h4><span class="pillar-num">b)</span> Gestione degli incidenti</h4>
<p><em>Sai cosa fare quando succede qualcosa?</em> Devi avere una procedura scritta che dica:
chi viene chiamato, in che ordine, chi notifica al CSIRT, chi parla con i giornalisti.</p>
</div>
<div class="pillar-card">
<h4><span class="pillar-num">c)</span> Continuità operativa e gestione delle crisi</h4>
<p><em>Se cade un server, in quanto tempo riparti?</em> RTO (tempo per ripartire) e RPO (quanti dati
puoi perdere) vanno definiti e <strong>testati</strong> almeno una volta l'anno.</p>
</div>
<div class="pillar-card">
<h4><span class="pillar-num">d)</span> Sicurezza della supply chain</h4>
<p><em>I tuoi fornitori sono sicuri?</em> Manda loro un questionario sicurezza,
chiedi le loro certificazioni, metti clausole NIS2 nei contratti.</p>
</div>
<div class="pillar-card">
<h4><span class="pillar-num">e)</span> Sicurezza nell'acquisto, sviluppo e manutenzione</h4>
<p><em>Quando comprate software, è sicuro?</em> Patch management, test prima del go-live, gestione
vulnerabilità (CVE).</p>
</div>
<div class="pillar-card">
<h4><span class="pillar-num">f)</span> Politiche di valutazione dell'efficacia</h4>
<p><em>Misuri se le tue misure funzionano?</em> Audit periodici (interni o esterni), KPI cyber,
revisione annuale del SoA (Statement of Applicability).</p>
</div>
<div class="pillar-card">
<h4><span class="pillar-num">g)</span> Igiene cibernetica di base e formazione</h4>
<p><em>I tuoi dipendenti sanno riconoscere un phishing?</em> Password manager, MFA, formazione
almeno annuale per tutti i livelli.</p>
</div>
<div class="pillar-card">
<h4><span class="pillar-num">h)</span> Crittografia</h4>
<p><em>I dati sensibili sono cifrati?</em> AES-256 per i dati a riposo, TLS 1.3 per i dati in
transito. Gestione corretta delle chiavi.</p>
</div>
<div class="pillar-card">
<h4><span class="pillar-num">i)</span> Sicurezza del personale, controllo accessi e gestione asset</h4>
<p><em>Chi può accedere a cosa?</em> Principio del minimo privilegio, revisione accessi 2 volte
l'anno, offboarding rapido quando un dipendente esce.</p>
</div>
<div class="pillar-card">
<h4><span class="pillar-num">j)</span> Autenticazione a più fattori (MFA)</h4>
<p><em>Hai MFA almeno su email, VPN, amministrazione?</em> Obbligatorio per tutti gli accessi
critici. Anche SMS è meglio di nulla, app authenticator è meglio di SMS, hardware token (FIDO2)
è il top.</p>
</div>
<div class="example-box">
<strong>Esempio Aurora Sanità.</strong> Punteggio iniziale 58%. I gap principali sono in
Supply Chain (40%) e Crittografia (50%). Piano di azione: assessment dei 12 fornitori critici
entro 6 mesi, attivare TLS 1.3 ovunque e cifratura at-rest sui DB cartelle cliniche entro 9 mesi.
</div>
<h3>Come si fa nella piattaforma</h3>
<ol class="step-list">
<li>Vai su <strong>Gap Analysis</strong> e clicca "Nuovo Assessment".</li>
<li>Rispondi alle 80 domande (sei modalità: implementato / parziale / non implementato / non applicabile).</li>
<li>Per ogni risposta, indica il <strong>livello di maturità</strong> (15).</li>
<li>Salva: puoi continuare in più sessioni.</li>
<li>Quando finisci, clicca <strong>"Analisi AI"</strong> per ricevere raccomandazioni prioritarie.</li>
</ol>
</section>
<!-- Cap 6 -->
<section id="cap-6" class="guide-section">
<h2><span class="num">6</span> Gestione dei Rischi</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> Il "registro dei rischi" è un elenco di cose brutte che
<em>potrebbero</em> accadere, con quanto è probabile e quanto farebbe male. Per ognuna decidi
cosa fare: ridurla, accettarla, assicurarla, o eliminarla.
</div>
<h3>La matrice 5×5</h3>
<p>Ogni rischio ha due valori:</p>
<ul>
<li><strong>Probabilità (15)</strong>: 1 = quasi mai, 5 = quasi certo.</li>
<li><strong>Impatto (15)</strong>: 1 = trascurabile, 5 = catastrofico.</li>
</ul>
<p>Il prodotto (probabilità × impatto) dà il <strong>punteggio di rischio</strong> da 1 a 25.
Sopra 16 è critico, 915 alto, 48 medio, sotto 4 basso.</p>
<h3>Le quattro strategie di trattamento</h3>
<ul>
<li><strong>Mitigare</strong>: riduco probabilità o impatto (es. installo backup offsite per ridurre l'impatto di un ransomware).</li>
<li><strong>Trasferire</strong>: passo il rischio a un altro (es. cyber-insurance).</li>
<li><strong>Accettare</strong>: il rischio è basso, lo accetto consapevolmente (con firma del board).</li>
<li><strong>Evitare</strong>: smetto di fare l'attività che genera il rischio.</li>
</ul>
<div class="example-box">
<strong>Esempio.</strong> Rischio "Ransomware su sistema PACS" (Aurora Sanità) → probabilità 4,
impatto 5 → punteggio inerente 20 (critico). Trattamento: mitigazione = MFA su RDP + backup
immutabili + patch mensili. Dopo le misure: probabilità 2, impatto 4 → punteggio residuo 8 (medio).
</div>
<h3>Come si fa nella piattaforma</h3>
<ol class="step-list">
<li>Vai su <strong>Rischi</strong> e clicca "Nuovo Rischio" (o "AI Suggerisci" per partire dai
rischi tipici del tuo settore).</li>
<li>Compila titolo, descrizione, categoria, minaccia e vulnerabilità.</li>
<li>Imposta probabilità e impatto inerenti (15 ciascuno).</li>
<li>Scegli la strategia di trattamento e descrivi le azioni concrete.</li>
<li>Imposta probabilità e impatto residui (dopo le misure).</li>
<li>Assegna un responsabile e una data di revisione.</li>
</ol>
<div class="norm-box">
<span class="article-tag">Art. 21 (2)(a)</span>
Le politiche di analisi dei rischi e di sicurezza dei sistemi informatici devono essere documentate,
approvate dagli organi di vertice, e aggiornate almeno una volta l'anno.
</div>
</section>
<!-- Cap 7 -->
<section id="cap-7" class="guide-section">
<h2><span class="num">7</span> Incidenti — gli obblighi 24h / 72h / 30d</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> Se hai un incidente "significativo" devi avvisare il CSIRT
(la squadra nazionale di risposta cyber) in tre tappe: una prima allerta entro 24 ore, una
notifica completa entro 72 ore, e un report finale entro 30 giorni.
</div>
<h3>Quando un incidente è "significativo"?</h3>
<p>Almeno uno di questi criteri:</p>
<ul>
<li>Ha colpito <strong>≥ 500 utenti</strong>.</li>
<li>Ha bloccato i servizi per <strong>&gt; 4 ore</strong>.</li>
<li>Ha <strong>impatto transfrontaliero</strong> (altri Stati UE).</li>
<li>È un <strong>cyber attack</strong> intenzionale (ransomware, DDoS, intrusione…).</li>
<li>Ha generato danni economici diretti &gt; <strong>100.000 €</strong>.</li>
</ul>
<h3>La timeline</h3>
<div class="pillar-card">
<h4><span class="pillar-num">24h</span> Early Warning</h4>
<p>Prima segnalazione "veloce". Bastano: cosa è successo, sospetti di malevolenza, impatto preliminare.
Non devi avere ancora tutte le risposte.</p>
</div>
<div class="pillar-card">
<h4><span class="pillar-num">72h</span> Notifica completa</h4>
<p>Aggiornamento dettagliato: indicatori di compromissione, sistemi colpiti, misure di contenimento
applicate, stima impatto.</p>
</div>
<div class="pillar-card">
<h4><span class="pillar-num">30d</span> Final Report</h4>
<p>Analisi completa: root cause, azioni correttive, lezioni apprese, raccomandazioni per il futuro.</p>
</div>
<div class="example-box">
<strong>Esempio.</strong> Aurora Sanità subisce un DDoS sul portale prenotazioni alle 09:00 di lunedì.
Sono colpiti 8.500 utenti per 4 ore → significativo. Workflow: Early Warning entro martedì 09:00 →
Notifica entro giovedì 09:00 → Final Report entro mercoledì 28 (30 giorni dopo).
</div>
<h3>Come si fa nella piattaforma</h3>
<ol class="step-list">
<li>Vai su <strong>Incidenti</strong> e clicca "Registra Incidente".</li>
<li>Compila titolo, classificazione, severità, ora di rilevazione.</li>
<li>Il sistema calcola automaticamente le 3 scadenze (24h/72h/30d).</li>
<li>Quando arriva il momento, usa i bottoni <strong>"Invia Early Warning"</strong>,
<strong>"Invia Notifica"</strong> e <strong>"Invia Final Report"</strong>: le email partono verso
l'indirizzo CSIRT configurato.</li>
<li>Aggiorna lo stato dell'incidente (analisi → contenimento → eradicazione → recovery → chiuso).</li>
<li>Compila root cause e lezioni apprese alla chiusura.</li>
</ol>
<div class="norm-box">
<span class="article-tag">Art. 23</span>
Stabilisce gli obblighi di segnalazione degli incidenti significativi al CSIRT competente. Il mancato
rispetto delle scadenze è sanzionabile fino al 2% del fatturato globale per le entità essenziali.
</div>
</section>
<!-- Cap 8 -->
<section id="cap-8" class="guide-section">
<h2><span class="num">8</span> Policy e procedure</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> Una "policy" è un documento scritto che dice <em>come</em>
si fa una cosa in azienda. Serve sia per fare bene, sia per dimostrare alle autorità che ci hai pensato.
</div>
<p>Le policy minime per NIS2:</p>
<ol>
<li>Politica di Sicurezza delle Informazioni (master).</li>
<li>Procedura di Gestione Incidenti.</li>
<li>Politica di Continuità Operativa (BCP/DR).</li>
<li>Politica di Controllo Accessi.</li>
<li>Politica di Crittografia.</li>
<li>Politica Supply Chain.</li>
<li>Politica Vulnerability Management.</li>
<li>Acceptable Use Policy (per i dipendenti).</li>
</ol>
<div class="example-box">
<strong>Esempio.</strong> Genera con l'AI la bozza della "Politica di Crittografia": clicchi
"Genera con AI", indichi il settore (sanità), l'AI ti propone un documento di 45 pagine con
sezioni standard (algoritmi accettati, gestione chiavi, KMS, audit). Tu rivedi, adatti e fai
approvare dal CDA. Il sistema marca la versione, la data di approvazione e la prossima revisione.
</div>
<h3>Stati di una policy</h3>
<p>Bozza → In revisione → Approvata → Pubblicata → Archiviata (quando sostituita).</p>
</section>
<!-- Cap 9 -->
<section id="cap-9" class="guide-section">
<h2><span class="num">9</span> Supply Chain (Fornitori)</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> Se un tuo fornitore IT viene bucato, l'attaccante può entrare
in casa tua. NIS2 ti obbliga a valutare i fornitori "critici" (quelli con accesso ai tuoi sistemi
o dati sensibili).
</div>
<h3>Cosa fare per ogni fornitore critico</h3>
<ol>
<li>Inserirlo nell'anagrafica con dati di contratto.</li>
<li>Classificare la criticità (low/medium/high/critical).</li>
<li>Inviargli il questionario sicurezza (40+ domande standard).</li>
<li>Verificare la risposta e assegnare un risk score (010).</li>
<li>Rinnovare la valutazione almeno una volta l'anno.</li>
<li>Inserire clausole NIS2 nel contratto (right to audit, notifica incidenti, etc.).</li>
</ol>
<div class="example-box">
<strong>Esempio.</strong> "MedTech Manutenzione PACS S.r.l." ha accesso remoto al PACS di
Aurora Sanità → criticità "critical". Risk score iniziale 6/10 (ha ISO 27001, ma sub-appalto
non chiaro). Azione: clausola contrattuale che vieta sub-appalto senza autorizzazione e
impone notifica incidenti entro 24h.
</div>
</section>
<!-- Cap 10 -->
<section id="cap-10" class="guide-section">
<h2><span class="num">10</span> Formazione (Art. 20)</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> NIS2 è chiara: <em>gli amministratori (CDA, direttori)
devono essere formati sulla cybersecurity</em>. Non è un nice-to-have, è obbligatorio. E sono
loro che rispondono in prima persona se l'azienda non è conforme.
</div>
<h3>Chi formare</h3>
<ol>
<li><strong>Organi di vertice</strong> (CDA, amministratori): formazione specifica annuale.</li>
<li><strong>Personale IT/sicurezza</strong>: formazione tecnica continua.</li>
<li><strong>Tutto il personale</strong>: awareness training (phishing, password, segnalazioni)
almeno annuale.</li>
</ol>
<div class="example-box">
<strong>Esempio.</strong> Aurora Sanità organizza: 1 corso da 4h per CDA, 1 corso da 16h per il team
IT, una sessione obbligatoria di 1h per tutti i dipendenti più simulazioni di phishing trimestrali.
Tutto loggato nel modulo Formazione.
</div>
<h3>Come si fa nella piattaforma</h3>
<ol class="step-list">
<li>Vai su <strong>Formazione</strong> &rarr; "Nuovo Corso".</li>
<li>Definisci titolo, contenuti, durata, ruoli target.</li>
<li>Assegna il corso ai dipendenti (singoli o per gruppo).</li>
<li>Imposta scadenza e prerequisiti (es. obbligatorio prima dell'onboarding).</li>
<li>Monitora il completamento dal cruscotto "Compliance Status".</li>
</ol>
<div class="norm-box">
<span class="article-tag">Art. 20</span>
Gli organi di gestione delle entità essenziali e importanti devono seguire una formazione che
consenta loro di acquisire conoscenze e competenze sufficienti per individuare i rischi e valutare
le pratiche di gestione del rischio di cibersicurezza.
</div>
</section>
<!-- Cap 11 -->
<section id="cap-11" class="guide-section">
<h2><span class="num">11</span> Asset (cosa hai e cosa proteggi)</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> Non puoi proteggere quello che non sai di avere.
L'inventario asset elenca tutti i sistemi, applicazioni, database e infrastrutture critiche.
</div>
<h3>Tipi di asset</h3>
<ul>
<li><strong>Hardware</strong>: server, PLC, dispositivi mobili.</li>
<li><strong>Software</strong>: applicazioni gestionali, SCADA, CRM.</li>
<li><strong>Network</strong>: firewall, router, switch.</li>
<li><strong>Data</strong>: database, file storage, backup.</li>
<li><strong>Service</strong>: portali web, API.</li>
<li><strong>Personnel</strong>: utenti privilegiati, ruoli chiave.</li>
<li><strong>Facility</strong>: data center, sale server.</li>
</ul>
<div class="example-box">
<strong>Esempio.</strong> Aurora Sanità inserisce: il PACS, il database cartelle cliniche,
il portale prenotazioni. Per ognuno: criticità, vendor, location, owner. Quando arriva un incidente
o un rischio, può essere associato a uno o più asset → tracciabilità totale.
</div>
</section>
<!-- Cap 12 -->
<section id="cap-12" class="guide-section">
<h2><span class="num">12</span> Audit &amp; Report</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> Quando le autorità (ACN) verranno a chiederti conto della tua
compliance, devi poter mostrare documenti, evidenze, registri. Qui li trovi tutti.
</div>
<h3>Cosa puoi fare</h3>
<ul>
<li><strong>Controlli</strong>: lista dei 10 controlli Art. 21 + mapping ISO 27001 con stato di
implementazione (0100%).</li>
<li><strong>Evidence Files</strong>: carica documenti probatori (verbali, screenshot, certificati).</li>
<li><strong>Audit Log</strong>: registro immutabile di tutto quello che succede in piattaforma
(utenti, modifiche, accessi). Garantito con catena hash SHA-256.</li>
<li><strong>Report Esecutivo</strong>: PDF/HTML stampabile per il CDA.</li>
<li><strong>Export CSV</strong>: rischi, incidenti, controlli, asset.</li>
</ul>
<div class="example-box">
<strong>Esempio.</strong> Pre-audit ISO 27001: genera il report esecutivo, esporta in CSV
rischi e controlli, scarica l'audit log certificato. L'auditor ha tutto in 5 minuti.
</div>
</section>
<!-- Cap 13 -->
<section id="cap-13" class="guide-section">
<h2><span class="num">13</span> Segnalazioni interne (Whistleblowing)</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> NIS2 incoraggia (e in alcuni casi obbliga) ad avere un canale
anonimo dove dipendenti e collaboratori possono segnalare problemi di sicurezza senza paura di
ritorsioni.
</div>
<h3>Tipi di segnalazione</h3>
<ul>
<li><strong>Whistleblowing</strong>: anonimo, comportamenti illeciti.</li>
<li><strong>Feedback bug/UX</strong>: segnalazioni operative sulla piattaforma stessa,
classificate dall'AI e (opzionalmente) risolte automaticamente.</li>
</ul>
<div class="example-box">
<strong>Esempio.</strong> Un dipendente nota che le credenziali admin del CRM sono salvate in
un foglio Excel condiviso. Invia una segnalazione anonima → il responsabile compliance la riceve
con codice di tracciamento → il dipendente può seguire lo stato senza rivelare la propria identità.
</div>
</section>
<!-- Cap 14 -->
<section id="cap-14" class="guide-section">
<h2><span class="num">14</span> AI — come usarla bene</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> La piattaforma usa Claude (Anthropic) per assisterti. L'AI
è bravissima a partire dal foglio bianco, ma <strong>non sostituisce</strong> il tuo giudizio:
rivedi sempre quello che produce prima di approvarlo.
</div>
<h3>Dove trovi l'AI</h3>
<ul>
<li><strong>Analisi Assessment</strong>: dopo aver completato la Gap Analysis, genera
un'analisi delle priorità con raccomandazioni.</li>
<li><strong>AI Suggerisci Rischi</strong>: parte dal tuo settore + asset e propone una lista
di rischi tipici.</li>
<li><strong>Genera Policy</strong>: bozza di policy a partire dalla categoria.</li>
<li><strong>Classifica Incidente</strong>: dato un incidente, suggerisce severità e classificazione.</li>
<li><strong>Knowledge Base RAG</strong>: chiedi all'AI partendo dai documenti che hai caricato.</li>
<li><strong>Classificazione Feedback</strong>: tag automatico per le segnalazioni di bug/UX.</li>
</ul>
<div class="example-box">
<strong>Buona pratica.</strong> Quando l'AI suggerisce 8 rischi, non importarli tutti ciecamente.
Scegli i 4 più rilevanti per il tuo contesto, scartane uno che è duplicato di un asset interno,
e personalizza descrizione/probabilità per la tua realtà.
</div>
<h3>Limiti che devi conoscere</h3>
<ul>
<li>L'AI non vede i tuoi dati grezzi: solo metadati anonimizzati (settore, range dipendenti,
categoria asset).</li>
<li>Le bozze di policy <strong>devono</strong> essere riviste da un umano competente prima
della pubblicazione.</li>
<li>L'AI non è un consulente legale né un sostituto del CISO.</li>
</ul>
</section>
<!-- Cap 15 -->
<section id="cap-15" class="guide-section">
<h2><span class="num">15</span> Glossario rapido</h2>
<dl class="glossario">
<dt><span class="acro">ACN</span> Agenzia per la Cybersicurezza Nazionale</dt>
<dd>L'autorità italiana che vigila sulla NIS2 e raccoglie le notifiche di incidenti.</dd>
<dt><span class="acro">CSIRT</span> Computer Security Incident Response Team</dt>
<dd>Squadra nazionale di risposta agli incidenti. Riceve le notifiche 24h/72h/30d.</dd>
<dt><span class="acro">ENISA</span> Agenzia UE per la Cybersicurezza</dt>
<dd>Coordina a livello europeo le linee guida operative NIS2.</dd>
<dt><span class="acro">RTO</span> Recovery Time Objective</dt>
<dd>Quanto tempo massimo puoi stare giù prima di tornare operativo. Esempio: RTO 4 ore.</dd>
<dt><span class="acro">RPO</span> Recovery Point Objective</dt>
<dd>Quanti dati puoi perderti al massimo. Esempio: RPO 1 ora = backup almeno orari.</dd>
<dt><span class="acro">MFA</span> Multi-Factor Authentication</dt>
<dd>Autenticazione a più fattori: password + codice OTP/app/hardware.</dd>
<dt><span class="acro">ISMS</span> Information Security Management System</dt>
<dd>Sistema di gestione della sicurezza delle informazioni (es. ISO 27001).</dd>
<dt><span class="acro">SoA</span> Statement of Applicability</dt>
<dd>Documento che elenca quali controlli ISO 27001 applichi e quali no, con motivazione.</dd>
<dt><span class="acro">BCP / DRP</span> Business Continuity Plan / Disaster Recovery Plan</dt>
<dd>I piani per garantire la continuità operativa e il ripristino dopo un disastro.</dd>
<dt><span class="acro">SCADA</span> Supervisory Control And Data Acquisition</dt>
<dd>Sistemi di controllo industriale (es. impianti di acqua, energia, manifattura).</dd>
<dt><span class="acro">PACS</span> Picture Archiving and Communication System</dt>
<dd>Sistema sanitario che archivia le immagini diagnostiche (TAC, RMN, ecografie).</dd>
<dt><span class="acro">TMS</span> Transport Management System</dt>
<dd>Software che gestisce le spedizioni e la flotta in azienda di logistica.</dd>
<dt><span class="acro">DDoS</span> Distributed Denial of Service</dt>
<dd>Attacco che satura un servizio con traffico inutile per metterlo offline.</dd>
<dt><span class="acro">BEC</span> Business Email Compromise</dt>
<dd>Frode via email che impersona dirigenti per ottenere bonifici fraudolenti.</dd>
<dt><span class="acro">NCR / CAPA</span> Non-Conformity Report / Corrective and Preventive Action</dt>
<dd>Non conformità (es. da audit) e relative azioni correttive/preventive.</dd>
<dt><span class="acro">RAG</span> Retrieval-Augmented Generation</dt>
<dd>Tecnica AI che fa rispondere l'assistente partendo da documenti caricati nella Knowledge Base.</dd>
</dl>
</section>
<div class="card" style="margin-top:40px;">
<div class="card-body" style="text-align:center;">
<p style="margin:0;color:var(--text-muted,#6b7280);">
Per dubbi specifici, in ogni pagina trovi il pulsante <strong>?</strong> in alto a destra
che apre la guida contestuale di quella sezione. Buon lavoro!
</p>
</div>
</div>
</div>
</div>
</main>
</div>
<script src="js/api.js"></script>
<script src="js/common.js"></script>
<script src="js/i18n.js"></script>
<script src="js/help.js"></script>
<script>
// La guida è accessibile anche da non autenticati (utile per onboarding)
// ma se sei loggato carichi sidebar + i18n normalmente.
if (typeof loadSidebar === 'function') loadSidebar();
if (typeof I18n !== 'undefined' && I18n.init) I18n.init();
</script>
</body>
</html>

1268
public/index-en.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NIS2 Agile — Compliance NIS2 semplificata per aziende e consulenti</title> <title>NIS2 Agile — Compliance NIS2 semplificata per aziende e consulenti</title>
<meta name="description" content="Piattaforma SaaS multi-tenant per la compliance alla Direttiva NIS2 (EU 2022/2555) e D.Lgs. 138/2024. Gap analysis AI, risk management, incident response Art.23, policy, formazione."> <meta name="description" content="Piattaforma SaaS multi-tenant per la compliance alla Direttiva NIS2 (EU 2022/2555) e D.Lgs. 138/2024. Gap analysis AI, risk management, incident response Art.23, policy, formazione.">
<link rel="canonical" href="https://nis2.agile.software/">
<link rel="alternate" hreflang="it" href="https://nis2.agile.software/">
<link rel="alternate" hreflang="en" href="https://nis2.agile.software/index-en.html">
<link rel="alternate" hreflang="x-default" href="https://nis2.agile.software/">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style> <style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
@ -623,6 +627,11 @@
margin-bottom: 16px; margin-bottom: 16px;
} }
/* ── NAV TOGGLE (hamburger mobile ≤768px) ── */
.nav-toggle { display: none; background: none; border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; cursor: pointer; padding: 8px 10px; color: var(--text-white); min-height: 40px; min-width: 40px; }
.nav-toggle:hover { background: rgba(255,255,255,0.08); border-color: var(--cyan); }
.nav-toggle svg { display: block; }
/* ── RESPONSIVE ── */ /* ── RESPONSIVE ── */
@media (max-width: 900px) { @media (max-width: 900px) {
nav { padding: 0 20px; } nav { padding: 0 20px; }
@ -637,11 +646,28 @@
.steps-wrap::before { display: none; } .steps-wrap::before { display: none; }
.lg231-card { flex-direction: column; padding: 28px; } .lg231-card { flex-direction: column; padding: 28px; }
.footer-inner { flex-direction: column; align-items: flex-start; } .footer-inner { flex-direction: column; align-items: flex-start; }
.nav-actions .btn-ghost { display: none; }
.form-grid { grid-template-columns: 1fr; } .form-grid { grid-template-columns: 1fr; }
.form-box { padding: 28px 20px; } .form-box { padding: 28px 20px; }
} }
@media (max-width: 768px) {
.nav-actions { display: none; }
.nav-actions.open {
display: flex; flex-direction: column; align-items: stretch;
position: fixed; top: 64px; left: 0; right: 0;
background: rgba(15,23,42,0.97); backdrop-filter: blur(16px);
padding: 20px; gap: 12px; z-index: 99;
border-bottom: 1px solid var(--border-color);
max-height: calc(100vh - 64px); overflow-y: auto;
}
.nav-actions.open .btn, .nav-actions.open .lang-switch-nis { width: 100%; justify-content: center; }
.nav-actions.open .lang-switch-nis { border-left: none; padding-left: 0; margin-left: 0; }
.nav-actions.open .evix-suite-pill { margin: 0; width: 100%; justify-content: center; }
.nav-toggle { display: inline-flex; align-items: center; justify-content: center; }
}
</style> </style>
<link rel="stylesheet" href="mobile-conversion.css?v=20260519a">
<!-- Plausible Analytics -->
<script defer data-domain="nis2.agile.software" src="https://analytics.agile.software/js/script.js"></script>
</head> </head>
<body> <body>
@ -651,14 +677,27 @@
<div class="nav-icon"><i class="fa-solid fa-shield-halved"></i></div> <div class="nav-icon"><i class="fa-solid fa-shield-halved"></i></div>
<span class="nav-name">NIS2 <span>Agile</span></span> <span class="nav-name">NIS2 <span>Agile</span></span>
</a> </a>
<div class="nav-actions"> <a href="https://evix.agile.software" target="_blank" rel="noopener" class="evix-suite-pill" title="Parte della EViX Suite — Scopri tutti i moduli" style="display:inline-flex;align-items:center;gap:8px;padding:7px 14px;background:linear-gradient(135deg,rgba(227,30,36,0.18),rgba(227,30,36,0.08));border:1.5px solid rgba(227,30,36,0.5);color:#FCA5A5;border-radius:100px;font-size:12px;font-weight:700;letter-spacing:0.04em;text-transform:uppercase;text-decoration:none;transition:all .2s;white-space:nowrap;margin-left:auto;margin-right:14px;">
<i class="fa-solid fa-layer-group" style="font-size:11px"></i>
<span class="evix-pill-label">EViX Suite</span>
<i class="fa-solid fa-arrow-up-right-from-square" style="font-size:9px;opacity:.75"></i>
</a>
<div class="nav-actions" id="navActions">
<a href="/login.html" class="btn btn-ghost btn-sm"> <a href="/login.html" class="btn btn-ghost btn-sm">
<i class="fa-solid fa-right-to-bracket"></i> Accedi <i class="fa-solid fa-right-to-bracket"></i> Accedi
</a> </a>
<a href="#richiedi-accesso" class="btn btn-primary btn-sm"> <a href="#richiedi-accesso" class="btn btn-primary btn-sm">
<i class="fa-solid fa-envelope"></i> Richiedi accesso <i class="fa-solid fa-envelope"></i> Richiedi accesso
</a> </a>
<div class="lang-switch-nis" style="display:flex;align-items:center;gap:4px;padding-left:10px;border-left:1px solid rgba(255,255,255,0.15);margin-left:6px;">
<a href="index.html" class="active" hreflang="it" style="color:#EF4444;text-decoration:none;padding:4px 8px;border-radius:5px;font-weight:700;font-size:12px;letter-spacing:0.04em;background:rgba(239,68,68,0.12);">IT</a>
<span style="color:rgba(255,255,255,0.3);font-size:12px;">|</span>
<a href="index-en.html" hreflang="en" style="color:#CBD5E1;text-decoration:none;padding:4px 8px;border-radius:5px;font-weight:700;font-size:12px;letter-spacing:0.04em;">EN</a>
</div>
</div> </div>
<button class="nav-toggle" onclick="document.getElementById('navActions').classList.toggle('open')" aria-label="Menu">
<svg width="22" height="22" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
</button>
</nav> </nav>
<!-- HERO --> <!-- HERO -->
@ -666,6 +705,14 @@
<div class="container"> <div class="container">
<div class="hero-wrap"> <div class="hero-wrap">
<div class="hero-left"> <div class="hero-left">
<a href="https://evix.agile.software" target="_blank" rel="noopener"
style="display:inline-flex;align-items:center;gap:8px;background:rgba(227,30,36,0.1);border:1px solid rgba(227,30,36,0.35);color:#E31E24;font-size:12.5px;font-weight:700;letter-spacing:0.04em;text-transform:uppercase;padding:7px 14px;border-radius:100px;text-decoration:none;margin-bottom:14px;transition:all .2s;"
onmouseover="this.style.background='rgba(227,30,36,0.18)';this.style.borderColor='#E31E24'"
onmouseout="this.style.background='rgba(227,30,36,0.1)';this.style.borderColor='rgba(227,30,36,0.35)'">
<i class="fa-solid fa-layer-group" style="font-size:11px"></i>
Modulo della EViX Suite
<i class="fa-solid fa-arrow-up-right-from-square" style="font-size:10px;opacity:.8"></i>
</a>
<div class="hero-badge"> <div class="hero-badge">
<i class="fa-solid fa-circle-check"></i> <i class="fa-solid fa-circle-check"></i>
D.Lgs. 138/2024 — In vigore dal 16 ottobre 2024 D.Lgs. 138/2024 — In vigore dal 16 ottobre 2024
@ -1013,12 +1060,23 @@
<textarea name="messaggio" placeholder="Descrivi brevemente la tua esigenza o il settore di appartenenza NIS2..."></textarea> <textarea name="messaggio" placeholder="Descrivi brevemente la tua esigenza o il settore di appartenenza NIS2..."></textarea>
</div> </div>
</div> </div>
<div class="form-group full" style="margin-top:4px">
<label style="display:flex;gap:8px;align-items:flex-start;cursor:pointer;font-weight:400;font-size:13px;color:var(--text-light)">
<input type="checkbox" name="consent" id="ahubConsent" required style="margin-top:4px;flex-shrink:0;accent-color:var(--cyan)">
<span>Acconsento al trattamento dei miei dati ai sensi del GDPR (art. 13). I dati saranno usati esclusivamente per rispondere alla richiesta. <a href="https://agile.software/privacy" target="_blank" style="color:var(--cyan)">Privacy policy</a> <span style="color:var(--cyan)">*</span></span>
</label>
</div>
<!-- Honeypot anti-bot -->
<div style="position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden" aria-hidden="true">
<label>Lascia vuoto</label>
<input type="text" name="website_url" tabindex="-1" autocomplete="off">
</div>
<button type="submit" class="btn btn-primary form-submit" id="submitBtn"> <button type="submit" class="btn btn-primary form-submit" id="submitBtn">
<i class="fa-solid fa-paper-plane"></i> Invia richiesta <i class="fa-solid fa-paper-plane"></i> Invia richiesta
</button> </button>
<p class="form-note"> <p class="form-note">
<i class="fa-solid fa-lock" style="color:var(--cyan);margin-right:4px;font-size:11px;"></i> <i class="fa-solid fa-lock" style="color:var(--cyan);margin-right:4px;font-size:11px;"></i>
I tuoi dati sono trattati nel rispetto del GDPR. Nessuna cessione a terzi. Risposta entro 24h lavorative · Powered by AgileHub
</p> </p>
</form> </form>
<div class="form-success" id="formSuccess"> <div class="form-success" id="formSuccess">
@ -1048,6 +1106,9 @@
<a href="/login.html" class="btn btn-ghost btn-lg"> <a href="/login.html" class="btn btn-ghost btn-lg">
<i class="fa-solid fa-right-to-bracket"></i> Accedi <i class="fa-solid fa-right-to-bracket"></i> Accedi
</a> </a>
<a href="https://evix.agile.software" target="_blank" rel="noopener" class="btn btn-ghost btn-lg" style="border-color:rgba(227,30,36,0.45);color:#E31E24">
<i class="fa-solid fa-layer-group"></i> Scopri tutta la EViX Suite
</a>
</div> </div>
<p class="cta-note">Sei un consulente o MSSP? Il form di richiesta ti permette di attivare accesso per il tuo intero portfolio clienti.</p> <p class="cta-note">Sei un consulente o MSSP? Il form di richiesta ti permette di attivare accesso per il tuo intero portfolio clienti.</p>
</div> </div>
@ -1065,7 +1126,13 @@
<div class="footer-links"> <div class="footer-links">
<a href="#richiedi-accesso">Richiedi accesso</a> <a href="#richiedi-accesso">Richiedi accesso</a>
<a href="/login.html">Accedi</a> <a href="/login.html">Accedi</a>
<a href="https://lg231.agile.software/" target="_blank">231 Agile</a> <a href="https://evix.agile.software" target="_blank" rel="noopener" style="color:#E31E24;font-weight:600">EViX Suite ↗</a>
<a href="https://lg231.agile.software/" target="_blank" rel="noopener">231 Agile</a>
<a href="https://qsa.agile.software/" target="_blank" rel="noopener">Agile QSA</a>
<a href="https://trpg.agile.software/" target="_blank" rel="noopener">TRPG Agile</a>
<a href="https://sustainai.agile.software/" target="_blank" rel="noopener">SustainAI</a>
<a href="https://platform.agile.software/" target="_blank" rel="noopener" style="color:#A78BFA;font-weight:600">Built on Agile Platform ↗</a>
<a href="https://agile.software/" target="_blank" rel="noopener">Agile Technology</a>
<a href="mailto:info@agile.software">info@agile.software</a> <a href="mailto:info@agile.software">info@agile.software</a>
</div> </div>
<div class="footer-copy"> <div class="footer-copy">
@ -1075,6 +1142,39 @@
</footer> </footer>
<script> <script>
/* AgileHub Lead Pipeline — NIS2 Agile (tenant agile-technology) */
const AHUB_PK = '02fd04c434fb5e1a83d11ee001f88e2677e8660f';
const AHUB_ENDPOINT = 'https://agilehub.agile.software/api/public/applets/' + AHUB_PK + '/submit';
/* Provenance tracking */
let __scrollMax = 0;
window.addEventListener('scroll', () => {
const h = document.documentElement;
const pct = Math.round((h.scrollTop || window.scrollY) / (h.scrollHeight - h.clientHeight) * 100);
if (pct > __scrollMax) __scrollMax = Math.min(pct, 100);
}, { passive: true });
const __pageLoadedAt = Date.now();
function ahubMetadata() {
const u = new URLSearchParams(location.search);
const md = {
utm_source: u.get('utm_source') || null,
utm_medium: u.get('utm_medium') || null,
utm_campaign: u.get('utm_campaign') || null,
utm_content: u.get('utm_content') || null,
utm_term: u.get('utm_term') || null,
referrer: document.referrer || null,
page_title: document.title,
page_url: location.href,
scroll_depth_pct: __scrollMax,
time_on_page_sec: Math.round((Date.now() - __pageLoadedAt) / 1000),
viewport: window.innerWidth + 'x' + window.innerHeight,
language: navigator.language,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
};
return md;
}
document.getElementById('inviteForm').addEventListener('submit', async function(e) { document.getElementById('inviteForm').addEventListener('submit', async function(e) {
e.preventDefault(); e.preventDefault();
const btn = document.getElementById('submitBtn'); const btn = document.getElementById('submitBtn');
@ -1082,35 +1182,52 @@ document.getElementById('inviteForm').addEventListener('submit', async function(
const successEl = document.getElementById('formSuccess'); const successEl = document.getElementById('formSuccess');
const form = e.target; const form = e.target;
/* Honeypot anti-bot — silent reject */
if (form.website_url && form.website_url.value) {
form.style.display = 'none';
successEl.style.display = 'block';
return;
}
if (!form.consent.checked) {
errEl.textContent = 'È necessario accettare il trattamento dati per inviare la richiesta.';
errEl.style.display = 'block';
return;
}
errEl.style.display = 'none'; errEl.style.display = 'none';
btn.disabled = true; btn.disabled = true;
btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Invio in corso...'; btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Invio in corso...';
const data = { const payload = {
name: form.nome.value.trim(), name: form.nome.value.trim(),
email: form.email.value.trim(), email: form.email.value.trim(),
phone: form.telefono.value.trim(), phone: form.telefono.value.trim(),
company: form.azienda.value.trim(), company: form.azienda.value.trim(),
tipo: form.tipo.value, tipo: form.tipo.value,
size: form.n_dipendenti.value, consent: true,
product_interest: form.interesse.value, fields: {
source: 'nis2-landing', n_dipendenti: form.n_dipendenti.value,
notes: form.messaggio.value.trim() interesse: form.interesse.value,
messaggio: form.messaggio.value.trim()
},
metadata: ahubMetadata()
}; };
try { try {
const res = await fetch('/api/mktg-lead/submit', { const res = await fetch(AHUB_ENDPOINT, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data) body: JSON.stringify(payload)
}); });
const json = await res.json(); const json = await res.json();
if (res.ok && json.success) { if (res.ok && json.success !== false) {
form.style.display = 'none'; form.style.display = 'none';
successEl.style.display = 'block'; successEl.style.display = 'block';
} else { } else {
errEl.textContent = json.message || 'Errore nell\'invio. Riprova o scrivici a info@agile.software.'; const code = json.error && json.error.code ? ' (' + json.error.code + ')' : '';
errEl.textContent = (json.error && json.error.message) || ('Errore nell\'invio' + code + '. Scrivici a info@agile.software.');
errEl.style.display = 'block'; errEl.style.display = 'block';
btn.disabled = false; btn.disabled = false;
btn.innerHTML = '<i class="fa-solid fa-paper-plane"></i> Invia richiesta'; btn.innerHTML = '<i class="fa-solid fa-paper-plane"></i> Invia richiesta';
@ -1123,5 +1240,19 @@ document.getElementById('inviteForm').addEventListener('submit', async function(
} }
}); });
</script> </script>
<!-- Mobile conversion layer — sticky CTA + trust ribbon -->
<script src="mobile-conversion.js?v=20260519a"
data-primary-text="Richiedi accesso"
data-primary-action="anchor"
data-primary-href="#richiedi-accesso"
data-primary-icon="→"
data-accent="#EF4444"
data-accent2="#DC2626"
data-trust-items='["D.Lgs. 138/2024","Gap analysis AI","Incident response Art.23","Multi-tenant SaaS"]'
data-trust-after="section#hero"
data-trust-theme="dark"
data-scroll-trigger="400"
defer></script>
</body> </body>
</html> </html>

548
public/js/ai-assistant.js Normal file
View File

@ -0,0 +1,548 @@
/**
* AgileHub AI Assistant Widget (L1 Self-Service)
* Embeddabile in qualsiasi prodotto della suite.
*
* Flusso: L1 AI (KB search) -> feedback -> L2 Formatore -> L3 Ticket
*
* Uso:
* <script src="js/ai-assistant.js"
* data-product="TRPG"
* data-tenant-id="3"
* data-api-url="https://agilehub.agile.software"
* data-user-name="Mario Rossi"
* data-user-email="mario@studio.it"
* data-user-role="consultant"
* data-lang="it">
* </script>
*
* Prerequisiti backend:
* - POST /api/support-sessions (crea sessione)
* - POST /api/support-sessions/:id/message (AI risponde con KB)
* - POST /api/support-sessions/:id/resolve (feedback positivo)
* - POST /api/support-sessions/:id/forward-expert (escala L2)
* - POST /api/support-sessions/:id/escalate (escala L3 ticket)
*/
(function () {
'use strict';
// ==================== CONFIG ====================
var script = document.currentScript || document.querySelector('script[data-product][data-api-url]');
var CFG = {
apiUrl: (script && script.dataset.apiUrl) || '',
product: (script && script.dataset.product) || 'GENERIC',
tenantId: (script && script.dataset.tenantId) || '1',
userName: (script && script.dataset.userName) || '',
userEmail: (script && script.dataset.userEmail) || '',
userRole: (script && script.dataset.userRole) || '',
lang: (script && script.dataset.lang) || 'it'
};
var LABELS = {
it: {
title: 'Assistente AgileHub',
placeholder: 'Scrivi una domanda...',
viewing: 'Stai guardando',
suggestions_intro: 'Posso aiutarti con:',
feedback_yes: 'Si, grazie',
feedback_no: 'No, aiuto',
escalation_title: 'Come posso aiutarti meglio?',
btn_rephrase: 'Riformula la domanda',
btn_expert: 'Chiedi al formatore',
btn_bug: 'Segnala un bug',
expert_sent: 'Domanda inoltrata al formatore. Riceverai una notifica quando la risposta sara pronta.',
expert_time: 'Tempo stimato: entro 4 ore lavorative.',
resolved_thanks: 'Grazie per il feedback! La tua valutazione migliora l\'assistente.',
preparing: 'Mi sto preparando...',
thinking: 'Sto pensando...',
source: 'Fonte',
resume: 'Riprendiamo la conversazione.',
mic_unsupported: 'Microfono non supportato dal browser.',
error_generic: 'Errore di connessione. Riprova tra poco.'
},
en: {
title: 'AgileHub Assistant',
placeholder: 'Ask a question...',
viewing: 'You are viewing',
suggestions_intro: 'I can help you with:',
feedback_yes: 'Yes, thanks',
feedback_no: 'No, help me',
escalation_title: 'How can I help you better?',
btn_rephrase: 'Rephrase question',
btn_expert: 'Ask an expert',
btn_bug: 'Report a bug',
expert_sent: 'Your question has been forwarded to an expert. You\'ll be notified when the answer is ready.',
expert_time: 'Estimated time: within 4 business hours.',
resolved_thanks: 'Thanks for the feedback! Your rating improves the assistant.',
preparing: 'Preparing...',
thinking: 'Thinking...',
source: 'Source',
resume: 'Let\'s pick up where we left off.',
mic_unsupported: 'Microphone not supported by this browser.',
error_generic: 'Connection error. Please try again shortly.'
}
};
function t(key) {
var lang = CFG.lang;
return (LABELS[lang] && LABELS[lang][key]) || (LABELS.it[key]) || key;
}
// ==================== STATE ====================
var sessionId = null;
var messages = []; // { role: 'user'|'assistant'|'system', content, sources?, feedbackShown? }
var panelOpen = false;
var recognition = null;
var isRecording = false;
// ==================== API HELPERS ====================
function apiHeaders() {
var h = { 'Content-Type': 'application/json', 'x-tenant-id': CFG.tenantId };
// Try to get JWT from the host app
if (typeof Api !== 'undefined' && Api.auth && Api.auth.getToken) {
var tok = Api.auth.getToken();
if (tok) h['Authorization'] = 'Bearer ' + tok;
}
var stored = typeof localStorage !== 'undefined' ? localStorage.getItem('nexus_token') : null;
if (!h['Authorization'] && stored) h['Authorization'] = 'Bearer ' + stored;
return h;
}
function apiPost(path, body) {
return fetch(CFG.apiUrl + path, {
method: 'POST', headers: apiHeaders(), body: JSON.stringify(body)
}).then(function (r) { return r.json(); });
}
function apiGet(path) {
return fetch(CFG.apiUrl + path, { headers: apiHeaders() }).then(function (r) { return r.json(); });
}
// ==================== CONTEXT DETECTION ====================
function detectPage() {
// Try HelpSystem if available
if (typeof HelpSystem !== 'undefined' && HelpSystem._detectPage) {
return HelpSystem._detectPage();
}
// Fallback: extract from URL hash or path
var hash = location.hash.replace('#', '').replace('/', '') || '';
return hash || document.title || '';
}
function getContextualSuggestions() {
if (typeof HelpSystem === 'undefined') return [];
var page = typeof HelpSystem._detectPage === 'function' ? HelpSystem._detectPage() : null;
var helpData = null;
if (page && HelpSystem._helpContent) helpData = HelpSystem._helpContent[page];
if (page && HelpSystem._help) {
var raw = HelpSystem._help[page];
if (raw) helpData = raw[CFG.lang] || raw.it || raw;
}
if (!helpData) return [];
var suggestions = [];
if (helpData.faq) {
suggestions = helpData.faq.slice(0, 3).map(function (f) {
return (typeof f === 'string') ? f : (f.question || f.q || '');
});
} else if (helpData.sections) {
suggestions = helpData.sections.slice(0, 3).map(function (s) {
if (!s.heading) return '';
if (typeof s.heading === 'object') return s.heading[CFG.lang] || s.heading.it || '';
return s.heading;
});
}
return suggestions.filter(Boolean);
}
// ==================== SESSION MANAGEMENT ====================
function createSession(cb) {
apiPost('/api/support-sessions', {
product: CFG.product,
callerEmail: CFG.userEmail,
callerName: CFG.userName
}).then(function (res) {
if (res.success && res.data) {
sessionId = res.data.sessionId || res.data.id;
if (res.data.welcomeMessage) {
messages.push({ role: 'assistant', content: res.data.welcomeMessage });
}
// Store session for resume
try { sessionStorage.setItem('agilehub_ai_session_' + CFG.product, sessionId); } catch (e) {}
}
if (cb) cb();
}).catch(function () {
messages.push({ role: 'system', content: t('error_generic') });
if (cb) cb();
});
}
function tryResumeSession(cb) {
try {
var stored = sessionStorage.getItem('agilehub_ai_session_' + CFG.product);
if (stored) {
sessionId = stored;
messages.push({ role: 'system', content: t('resume') });
cb(true);
return;
}
} catch (e) {}
cb(false);
}
function sendMessage(text, cb) {
if (!sessionId) {
createSession(function () { if (sessionId) sendMessage(text, cb); else cb(); });
return;
}
messages.push({ role: 'user', content: text });
render();
apiPost('/api/support-sessions/' + sessionId + '/message', {
content: text
}).then(function (res) {
if (res.success && res.data) {
var reply = res.data.reply || res.data.message || res.data.answer || '';
var sources = res.data.knowledgeArticles || res.data.sources || [];
messages.push({ role: 'assistant', content: reply, sources: sources, feedbackShown: true });
} else {
messages.push({ role: 'assistant', content: (res.error && res.error.message) || t('error_generic') });
}
cb();
}).catch(function () {
messages.push({ role: 'assistant', content: t('error_generic') });
cb();
});
}
function resolveSession() {
if (!sessionId) return;
apiPost('/api/support-sessions/' + sessionId + '/resolve', {
rating: 5, resolutionSummary: 'auto'
});
}
function forwardToExpert(question, context) {
if (!sessionId) return;
return apiPost('/api/support-sessions/' + sessionId + '/forward-expert', {
question: question, context: context
});
}
function escalateToTicket(reason) {
if (!sessionId) return;
return apiPost('/api/support-sessions/' + sessionId + '/escalate', {
reason: reason
});
}
// ==================== VOICE INPUT ====================
function initVoice() {
var SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SR) return null;
recognition = new SR();
recognition.lang = CFG.lang === 'en' ? 'en-US' : 'it-IT';
recognition.interimResults = true;
recognition.continuous = false;
return recognition;
}
function toggleMic() {
if (!recognition) {
if (!initVoice()) {
messages.push({ role: 'system', content: t('mic_unsupported') });
render();
return;
}
}
if (isRecording) {
recognition.stop();
isRecording = false;
render();
return;
}
var input = document.getElementById('aia-input');
recognition.onresult = function (e) {
var transcript = '';
for (var i = e.resultIndex; i < e.results.length; i++) {
transcript += e.results[i][0].transcript;
}
if (input) input.value = transcript;
if (e.results[e.results.length - 1].isFinal) {
isRecording = false;
render();
if (transcript.trim()) submitInput(transcript.trim());
}
};
recognition.onerror = function () { isRecording = false; render(); };
recognition.onend = function () { isRecording = false; render(); };
recognition.start();
isRecording = true;
render();
}
// ==================== RENDERING ====================
function esc(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c];
});
}
function renderMessages() {
return messages.map(function (m, idx) {
var isUser = m.role === 'user';
var isSystem = m.role === 'system';
var bubble = '<div style="margin-bottom:10px;display:flex;justify-content:'
+ (isUser ? 'flex-end' : 'flex-start') + '">'
+ '<div style="max-width:82%;padding:10px 14px;border-radius:14px;font-size:13px;line-height:1.5;'
+ 'background:' + (isUser ? 'linear-gradient(135deg,#7C3AED,#3B82F6)' : isSystem ? '#FEF3C7' : '#f3f4f6')
+ ';color:' + (isUser ? '#fff' : '#111827')
+ ';white-space:pre-wrap">'
+ esc(m.content);
// Sources
if (m.sources && m.sources.length > 0) {
bubble += '<div style="margin-top:8px;padding-top:6px;border-top:1px solid rgba(0,0,0,.1);font-size:11px;color:#6B7280">';
m.sources.forEach(function (s, si) {
bubble += '<div>' + t('source') + ' [' + (si + 1) + '] ' + esc(s.question || s.title || '') + '</div>';
});
bubble += '</div>';
}
bubble += '</div></div>';
// Feedback buttons
if (m.feedbackShown && m.role === 'assistant' && idx === messages.length - 1) {
bubble += '<div id="aia-feedback" style="display:flex;gap:8px;justify-content:flex-start;margin-bottom:10px">'
+ '<button onclick="window.__aiaFeedback(true)" style="padding:6px 14px;border-radius:8px;border:1px solid #D1D5DB;'
+ 'background:#fff;cursor:pointer;font-size:12px;color:#059669">&#x1F44D; ' + esc(t('feedback_yes')) + '</button>'
+ '<button onclick="window.__aiaFeedback(false)" style="padding:6px 14px;border-radius:8px;border:1px solid #D1D5DB;'
+ 'background:#fff;cursor:pointer;font-size:12px;color:#DC2626">&#x1F44E; ' + esc(t('feedback_no')) + '</button>'
+ '</div>';
}
return bubble;
}).join('');
}
function renderEscalation() {
return '<div id="aia-escalation" style="background:#F9FAFB;border-radius:12px;padding:14px;margin-bottom:10px">'
+ '<div style="font-weight:600;margin-bottom:10px;font-size:13px">' + esc(t('escalation_title')) + '</div>'
+ '<button onclick="window.__aiaEscalate(\'rephrase\')" style="display:block;width:100%;text-align:left;padding:10px 12px;'
+ 'border:1px solid #E5E7EB;border-radius:8px;background:#fff;cursor:pointer;margin-bottom:6px;font-size:12px">'
+ '&#x1F504; ' + esc(t('btn_rephrase')) + '</button>'
+ '<button onclick="window.__aiaEscalate(\'expert\')" style="display:block;width:100%;text-align:left;padding:10px 12px;'
+ 'border:1px solid #E5E7EB;border-radius:8px;background:#fff;cursor:pointer;margin-bottom:6px;font-size:12px">'
+ '&#x1F468;&#x200D;&#x1F3EB; ' + esc(t('btn_expert')) + '</button>'
+ '<button onclick="window.__aiaEscalate(\'bug\')" style="display:block;width:100%;text-align:left;padding:10px 12px;'
+ 'border:1px solid #E5E7EB;border-radius:8px;background:#fff;cursor:pointer;font-size:12px">'
+ '&#x1F41B; ' + esc(t('btn_bug')) + '</button>'
+ '</div>';
}
function render() {
var panel = document.getElementById('aia-panel');
if (!panel) return;
var page = detectPage();
var suggestions = (messages.length === 0) ? getContextualSuggestions() : [];
var showEscalation = panel.dataset.showEscalation === '1';
panel.innerHTML =
// Header
'<div style="padding:14px 16px;background:linear-gradient(135deg,#7C3AED,#3B82F6);color:#fff;'
+ 'display:flex;justify-content:space-between;align-items:center;border-radius:16px 16px 0 0">'
+ '<div style="display:flex;align-items:center;gap:8px;font-weight:600;font-size:14px">'
+ '<i class="fa-solid fa-wand-magic-sparkles"></i> ' + esc(t('title'))
+ '</div>'
+ '<button onclick="window.__aiaToggle()" style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer">&times;</button>'
+ '</div>'
// Context bar
+ (page ? '<div style="padding:8px 16px;background:#EDE9FE;font-size:11px;color:#6D28D9">'
+ '&#x1F4CD; ' + esc(t('viewing')) + ': <strong>' + esc(page) + '</strong></div>' : '')
// Messages area
+ '<div id="aia-msgs" style="flex:1;overflow-y:auto;padding:14px 16px;background:#fafafa;min-height:200px">'
+ (messages.length === 0 && suggestions.length > 0
? '<div style="color:#6B7280;font-size:13px;margin-bottom:12px">' + esc(t('suggestions_intro')) + '</div>'
+ suggestions.map(function (s) {
return '<button onclick="window.__aiaSuggest(\'' + esc(s).replace(/'/g, "\\'") + '\')" '
+ 'style="display:block;width:100%;text-align:left;padding:8px 12px;margin-bottom:6px;'
+ 'border:1px solid #E5E7EB;border-radius:8px;background:#fff;cursor:pointer;font-size:12px;color:#4B5563">'
+ '&#x1F4A1; ' + esc(s) + '</button>';
}).join('')
: '')
+ renderMessages()
+ (showEscalation ? renderEscalation() : '')
+ '</div>'
// Input bar
+ '<form id="aia-form" onsubmit="return window.__aiaSubmit(event)" '
+ 'style="display:flex;gap:6px;padding:10px;border-top:1px solid #E5E7EB;background:#fff;border-radius:0 0 16px 16px">'
+ '<input id="aia-input" type="text" placeholder="' + esc(t('placeholder')) + '" autocomplete="off" '
+ 'style="flex:1;padding:10px 12px;border:1px solid #D1D5DB;border-radius:10px;font-size:13px;font-family:inherit">'
+ '<button type="button" onclick="window.__aiaMic()" style="padding:8px;border:none;border-radius:10px;'
+ 'background:' + (isRecording ? '#EF4444' : '#F3F4F6') + ';cursor:pointer;font-size:16px" title="Voce">'
+ (isRecording ? '&#x23F9;' : '&#x1F3A4;') + '</button>'
+ '<button type="submit" style="padding:8px 14px;border:none;border-radius:10px;'
+ 'background:linear-gradient(135deg,#7C3AED,#3B82F6);color:#fff;font-weight:600;cursor:pointer">'
+ '<i class="fa-solid fa-paper-plane"></i></button>'
+ '</form>';
// Scroll to bottom
var msgsEl = document.getElementById('aia-msgs');
if (msgsEl) msgsEl.scrollTop = msgsEl.scrollHeight;
}
// ==================== HANDLERS ====================
function submitInput(text) {
if (!text) return;
var input = document.getElementById('aia-input');
if (input) input.value = '';
var panel = document.getElementById('aia-panel');
if (panel) panel.dataset.showEscalation = '0';
// Show thinking
messages.push({ role: 'assistant', content: t('thinking') });
render();
messages.pop(); // remove thinking
sendMessage(text, function () { render(); });
}
window.__aiaToggle = function () {
panelOpen = !panelOpen;
var panel = document.getElementById('aia-panel');
var fab = document.getElementById('ai-chat-fab');
if (panel) panel.style.display = panelOpen ? 'flex' : 'none';
if (fab) fab.style.display = panelOpen ? 'none' : 'flex';
if (panelOpen && messages.length === 0) {
tryResumeSession(function (resumed) {
if (!resumed) {
createSession(function () { render(); });
} else {
render();
}
});
}
if (panelOpen) render();
};
window.__aiaSubmit = function (e) {
e.preventDefault();
var input = document.getElementById('aia-input');
var text = (input && input.value || '').trim();
if (text) submitInput(text);
return false;
};
window.__aiaSuggest = function (text) { submitInput(text); };
window.__aiaMic = function () { toggleMic(); };
window.__aiaFeedback = function (positive) {
var fbEl = document.getElementById('aia-feedback');
if (fbEl) fbEl.remove();
if (positive) {
resolveSession();
messages.push({ role: 'system', content: t('resolved_thanks') });
render();
} else {
// Show escalation options
var panel = document.getElementById('aia-panel');
if (panel) panel.dataset.showEscalation = '1';
render();
}
};
window.__aiaEscalate = function (action) {
var panel = document.getElementById('aia-panel');
if (panel) panel.dataset.showEscalation = '0';
if (action === 'rephrase') {
render();
var input = document.getElementById('aia-input');
if (input) input.focus();
return;
}
if (action === 'expert') {
var lastUserMsg = '';
var lastAiMsg = '';
for (var i = messages.length - 1; i >= 0; i--) {
if (!lastAiMsg && messages[i].role === 'assistant') lastAiMsg = messages[i].content;
if (!lastUserMsg && messages[i].role === 'user') lastUserMsg = messages[i].content;
if (lastUserMsg && lastAiMsg) break;
}
forwardToExpert(lastUserMsg, lastAiMsg).then(function () {
messages.push({ role: 'system', content: t('expert_sent') + '\n' + t('expert_time') });
render();
});
return;
}
if (action === 'bug') {
var reason = '';
for (var j = messages.length - 1; j >= 0; j--) {
if (messages[j].role === 'user') { reason = messages[j].content; break; }
}
escalateToTicket(reason).then(function () {
messages.push({ role: 'system', content: 'Ticket tecnico creato. Il team di sviluppo lo prendera in carico.' });
render();
});
return;
}
};
// ==================== INIT ====================
function init() {
// Ensure FAB exists (may have been created by product's app.js)
var fab = document.getElementById('ai-chat-fab');
if (!fab) {
fab = document.createElement('button');
fab.id = 'ai-chat-fab';
fab.setAttribute('aria-label', 'Chiedi ad ARIA');
fab.style.cssText = 'position:fixed;bottom:24px;right:24px;width:56px;height:56px;'
+ 'border-radius:50%;border:none;cursor:pointer;z-index:9998;'
+ 'background:linear-gradient(135deg,#7C3AED,#3B82F6);color:#fff;'
+ 'box-shadow:0 6px 20px rgba(124,58,237,.4);font-size:22px;'
+ 'display:flex;align-items:center;justify-content:center';
fab.innerHTML = '<i class="fa-solid fa-wand-magic-sparkles"></i>';
document.body.appendChild(fab);
} else {
// Rimuovi il vecchio pannello AI del prodotto (evita doppia finestra)
var oldPanel = document.getElementById('ai-chat-panel');
if (oldPanel) oldPanel.remove();
// Clona il FAB per eliminare TUTTI gli addEventListener precedenti
var cleanFab = fab.cloneNode(true);
fab.parentNode.replaceChild(cleanFab, fab);
fab = cleanFab;
}
fab.style.display = 'flex';
fab.style.alignItems = 'center';
fab.style.justifyContent = 'center';
fab.onclick = window.__aiaToggle;
// Ensure panel exists
var panel = document.getElementById('aia-panel');
if (!panel) {
panel = document.createElement('div');
panel.id = 'aia-panel';
panel.style.cssText = 'position:fixed;bottom:90px;right:24px;width:380px;max-height:560px;'
+ 'background:#fff;border-radius:16px;box-shadow:0 12px 32px rgba(0,0,0,.2);'
+ 'display:none;z-index:9999;overflow:hidden;flex-direction:column;'
+ 'font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif';
document.body.appendChild(panel);
}
panel.dataset.showEscalation = '0';
}
// Auto-init when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

870
public/js/bug-reporter.js Normal file
View File

@ -0,0 +1,870 @@
/**
* AgileHub Bug Reporter Widget
* Embeddabile in qualsiasi prodotto (AllTax, TRPG, DFM, WMS, ecc.)
*
* Features:
* - Bottone 🐛 segnalazione bug (testo + voce)
* - Ctrl+V paste screenshot dalla clipboard
* - Bottone 📸 screenshot rapido della videata
* - Drag & drop immagini
* - Tab "Mie Segnalazioni" / "Risolte"
* - Campanella 🔔 notifiche con badge
*
* Uso:
* <script src="https://agilehub.agile.software/widgets/bug-reporter.js"
* data-product="TRPG"
* data-tenant-id="1"
* data-api-url="https://agilehub.agile.software"
* data-user-name="Marco Consulente"
* data-user-email="marco@studio.it"
* data-user-role="consultant">
* </script>
*/
(function() {
'use strict';
// ==================== CONFIG ====================
const script = document.currentScript || document.querySelector('script[data-product]');
const WIDGET_VERSION = '1.4.15';
const WIDGET_BUILD = '20260415d';
const CFG = {
apiUrl: (script && script.dataset.apiUrl) || 'http://localhost',
apiKey: (script && script.dataset.apiKey) || '',
product: (script && script.dataset.product) || '',
tenantId: (script && script.dataset.tenantId) || '1',
userName: (script && script.dataset.userName) || '',
userEmail: (script && script.dataset.userEmail) || '',
userRole: (script && script.dataset.userRole) || '',
lang: (script && script.dataset.lang) || 'it'
};
// L'API è sempre via proxy /api (sia per la dashboard che per i prodotti esterni).
const API = CFG.apiUrl + '/api';
// Headers standard per tutte le chiamate API
function _headers(json) {
const h = { 'x-tenant-id': CFG.tenantId };
if (CFG.apiKey) h['X-API-Key'] = CFG.apiKey;
if (json) h['Content-Type'] = 'application/json';
return h;
}
// Auto-detect utente dal contesto dell'app (se data-user-email non è nel tag)
function _detectUser() {
if (CFG.userEmail) return;
try {
// 1. sessionStorage 'user' (pattern TRPG — JSON con email/name/role)
const userJson = sessionStorage.getItem('user');
if (userJson) {
const u = JSON.parse(userJson);
if (u && u.email) { CFG.userEmail = u.email; CFG.userName = u.name || u.full_name || CFG.userName; CFG.userRole = u.role || CFG.userRole; return; }
}
// 2. api.getUser() (pattern TRPG/AllTax — globale)
if (typeof api !== 'undefined') {
const u = (typeof api.getUser === 'function' ? api.getUser() : api.user) || {};
if (u.email) { CFG.userEmail = u.email; CFG.userName = u.name || u.full_name || CFG.userName; CFG.userRole = u.role || CFG.userRole; return; }
}
// 3. JWT decode da access_token / nexus_token
const token = sessionStorage.getItem('access_token') || sessionStorage.getItem('nexus_token') || localStorage.getItem('nexus_token');
if (token && token.includes('.')) {
const payload = JSON.parse(atob(token.split('.')[1].replace(/-/g,'+').replace(/_/g,'/')));
if (payload.email) { CFG.userEmail = payload.email; CFG.userName = payload.name || payload.full_name || CFG.userName; CFG.userRole = payload.role || CFG.userRole; }
}
} catch(e) { /* silent */ }
}
// Ritenta: lo script è nel HEAD, l'utente potrebbe non essere ancora loggato
setTimeout(_detectUser, 500);
setTimeout(_detectUser, 3000);
setTimeout(_detectUser, 8000);
// ==================== STATE ====================
let _attachments = [];
let _notifCount = 0;
let _notifPollTimer = null;
let _supportSessionId = null;
let _chatMessages = [];
let _chatMode = false; // AI chat e nel widget separato ai-assistant.js
let _chatLoading = false;
// ==================== STYLES ====================
const STYLES = `
#nx-bar{position:fixed;bottom:94px;right:24px;display:flex;flex-direction:column;gap:14px;z-index:99990;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}
#nx-bar button{width:56px;height:56px;border-radius:50%;border:none;cursor:pointer;font-size:22px;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 12px rgba(0,0,0,.25);transition:transform .15s}
#nx-bar button:hover{transform:scale(1.1)}
#nx-bug-btn{background:#ef4444;color:#fff}
#nx-bell-btn{background:#3b82f6;color:#fff;position:relative}
#nx-bell-badge{position:absolute;top:-4px;right:-4px;background:#ef4444;color:#fff;border-radius:10px;min-width:18px;height:18px;font-size:10px;font-weight:700;display:flex;align-items:center;justify-content:center;padding:0 4px}
#nx-modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:99991;display:flex;align-items:center;justify-content:center}
#nx-modal{background:#fff;border-radius:14px;width:92%;max-width:500px;max-height:92vh;overflow:hidden;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,.3)}
#nx-modal *{box-sizing:border-box}
.nx-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #e5e7eb}
.nx-header h3{margin:0;font-size:17px;font-weight:600}
.nx-close{cursor:pointer;font-size:20px;color:#999;background:none;border:none;width:auto;height:auto;box-shadow:none}
.nx-close:hover{color:#333;transform:none}
.nx-tabs{display:flex;border-bottom:1px solid #e5e7eb}
.nx-tab{flex:1;padding:10px;text-align:center;font-size:13px;font-weight:500;cursor:pointer;border:none;background:none;color:#6b7280;border-bottom:2px solid transparent;transition:all .15s}
.nx-tab.active{color:#3b82f6;border-bottom-color:#3b82f6}
.nx-body{flex:1;overflow-y:auto;padding:16px 20px}
.nx-row{display:flex;gap:8px;margin-bottom:10px}
.nx-select,.nx-input,.nx-textarea{width:100%;padding:9px 12px;border-radius:8px;border:1px solid #d1d5db;font-size:14px;font-family:inherit}
.nx-textarea{height:90px;resize:vertical}
.nx-textarea.drag-over{border-color:#3b82f6;background:#eff6ff}
.nx-btn{padding:10px;border-radius:8px;border:1px solid #d1d5db;cursor:pointer;background:#f9fafb;font-size:14px;text-align:center;flex:1}
.nx-btn:hover{background:#f3f4f6}
.nx-btn-primary{background:#3b82f6;color:#fff;border:none;font-weight:600;width:100%;padding:12px;font-size:15px}
.nx-btn-primary:hover{background:#2563eb}
.nx-btn-primary:disabled{opacity:.5;cursor:not-allowed}
.nx-att-list{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px}
.nx-att-item{position:relative;width:60px;height:60px;border-radius:6px;overflow:hidden;border:1px solid #e5e7eb}
.nx-att-item img{width:100%;height:100%;object-fit:cover}
.nx-att-remove{position:absolute;top:-4px;right:-4px;width:18px;height:18px;border-radius:50%;background:#ef4444;color:#fff;font-size:11px;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1}
.nx-ctx{background:#f8fafc;border-radius:8px;padding:8px 12px;margin-bottom:10px;font-size:12px;color:#6b7280}
.nx-result{margin-top:12px;padding:12px;border-radius:8px;font-size:14px}
.nx-result.ok{background:#f0fdf4;border:1px solid #bbf7d0;color:#166534}
.nx-result.err{background:#fef2f2;border:1px solid #fecaca;color:#991b1b}
.nx-ticket{padding:12px;border:1px solid #e5e7eb;border-radius:10px;margin-bottom:8px;cursor:pointer;transition:background .1s}
.nx-ticket:hover{background:#f9fafb}
.nx-ticket-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}
.nx-ticket-id{font-size:12px;font-weight:600;color:#3b82f6}
.nx-ticket-status{font-size:11px;padding:2px 8px;border-radius:10px;font-weight:500}
.nx-ticket-subject{font-size:14px;font-weight:500;margin-bottom:4px}
.nx-ticket-meta{font-size:12px;color:#9ca3af}
.nx-ticket-ai{font-size:13px;color:#7c3aed;background:#f5f3ff;padding:8px;border-radius:6px;margin-top:6px}
.nx-notif{padding:10px 12px;border-bottom:1px solid #f3f4f6;cursor:pointer;transition:background .1s}
.nx-notif:hover{background:#f9fafb}
.nx-notif.unread{background:#eff6ff}
.nx-notif-title{font-size:14px;font-weight:500;margin-bottom:2px}
.nx-notif-body{font-size:13px;color:#6b7280}
.nx-notif-meta{font-size:11px;color:#9ca3af;margin-top:4px}
.nx-notif-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px}
.nx-notif-dot.BUG_FIX{background:#22c55e}.nx-notif-dot.UPDATE{background:#3b82f6}
.nx-notif-dot.ALERT{background:#ef4444}.nx-notif-dot.INFO{background:#9ca3af}
.nx-empty{text-align:center;padding:30px;color:#9ca3af;font-size:14px}
.nx-chat-area{flex:1;overflow-y:auto;padding:12px;min-height:200px;max-height:50vh;display:flex;flex-direction:column;gap:8px}
.nx-chat-msg{max-width:85%;padding:10px 14px;border-radius:14px;font-size:14px;line-height:1.5;word-wrap:break-word;animation:nxFadeIn .2s}
.nx-chat-msg.user{align-self:flex-end;background:#4f46e5;color:#fff;border-bottom-right-radius:4px}
.nx-chat-msg.ai{align-self:flex-start;background:#f3f4f6;color:#1f2937;border-bottom-left-radius:4px}
.nx-chat-msg.system{align-self:center;background:#fef3c7;color:#92400e;font-size:13px;border-radius:8px;text-align:center}
.nx-chat-input{display:flex;gap:8px;padding:12px;border-top:1px solid #e5e7eb}
.nx-chat-input input{flex:1;padding:10px 14px;border:1px solid #d1d5db;border-radius:20px;font-size:14px;font-family:inherit;outline:none}
.nx-chat-input input:focus{border-color:#4f46e5}
.nx-chat-input button{width:40px;height:40px;border-radius:50%;border:none;background:#4f46e5;color:#fff;cursor:pointer;font-size:16px;display:flex;align-items:center;justify-content:center}
.nx-chat-input button:disabled{opacity:.4;cursor:not-allowed}
.nx-chat-actions{display:flex;gap:8px;padding:0 12px 12px;justify-content:center}
.nx-chat-actions button{padding:8px 16px;border-radius:8px;border:1px solid #d1d5db;background:#f9fafb;font-size:13px;cursor:pointer;font-family:inherit}
.nx-chat-actions button:hover{background:#f3f4f6}
.nx-chat-actions .resolve{border-color:#22c55e;color:#16a34a}
.nx-chat-actions .classic{border-color:#3b82f6;color:#2563eb}
.nx-typing{display:flex;gap:4px;padding:6px 14px;align-self:flex-start}
.nx-typing span{width:6px;height:6px;border-radius:50%;background:#9ca3af;animation:nxBounce .6s infinite alternate}
.nx-typing span:nth-child(2){animation-delay:.15s}
.nx-typing span:nth-child(3){animation-delay:.3s}
@keyframes nxFadeIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}
@keyframes nxBounce{to{transform:translateY(-4px);opacity:.4}}
.nx-status-OPEN{background:#dbeafe;color:#1d4ed8}
.nx-status-IN_PROGRESS{background:#fef3c7;color:#92400e}
.nx-status-PENDING{background:#f3f4f6;color:#4b5563}
.nx-status-RESOLVED{background:#d1fae5;color:#065f46}
.nx-status-CLOSED{background:#e5e7eb;color:#6b7280}
`;
// ==================== INIT ====================
function init() {
// Inject styles
const style = document.createElement('style');
style.textContent = STYLES;
document.head.appendChild(style);
// Bottom bar: 🛟 supporto + 🔔 notifiche
// Standard suite Agile: bug=fa-life-ring "Chiedi supporto" / bell=fa-bell
const bar = document.createElement('div');
bar.id = 'nx-bar';
bar.innerHTML = `
<button id="nx-bell-btn" title="Notifiche"><i class="fas fa-bell"></i><span id="nx-bell-badge" style="display:none">0</span></button>
<button id="nx-bug-btn" title="Chiedi supporto"><i class="fas fa-life-ring"></i></button>
`;
document.body.appendChild(bar);
document.getElementById('nx-bug-btn').onclick = () => openModal('new');
document.getElementById('nx-bell-btn').onclick = () => openModal('notif');
// Hook campanella topbar del prodotto (se esiste)
const topBell = document.getElementById('btn-notifications');
if (topBell) {
// Rimuovi il vecchio dropdown notifiche del prodotto (evita doppia finestra)
const oldDropdown = document.getElementById('notif-dropdown');
if (oldDropdown) oldDropdown.remove();
// Clona per rimuovere vecchi listener
const newBell = topBell.cloneNode(true);
topBell.parentNode.replaceChild(newBell, topBell);
newBell.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); openModal('notif'); });
window._nxTopBadge = function(c) {
const b = newBell.querySelector('.badge') || document.getElementById('worklist-badge');
if (b) { b.textContent = c > 99 ? '99+' : c; b.style.display = c > 0 ? '' : 'none'; }
};
}
// Start notification polling
pollNotifications();
_notifPollTimer = setInterval(pollNotifications, 60000);
}
// ==================== NOTIFICATION POLLING ====================
async function pollNotifications() {
try {
const res = await fetch(`${API}/notifications/unread-count?product=${CFG.product}`, {
headers: _headers()
});
const d = await res.json();
if (d.success) {
_notifCount = d.data.unread;
const badge = document.getElementById('nx-bell-badge');
if (badge) {
badge.style.display = _notifCount > 0 ? 'flex' : 'none';
badge.textContent = _notifCount > 9 ? '9+' : String(_notifCount);
}
// Aggiorna anche campanella topbar del prodotto (se hookata)
if (window._nxTopBadge) window._nxTopBadge(_notifCount);
}
} catch(e) { /* silent */ }
}
// ==================== MODAL ====================
function openModal(tab) {
closeModal();
_attachments = [];
_chatMode = false; // AI chat e nel widget separato ai-assistant.js
_supportSessionId = null;
_chatMessages = [];
// Nasconde FAB ARIA quando il modale segnalazioni e aperto
var aiaFab = document.getElementById('ai-chat-fab');
if (aiaFab) aiaFab.style.display = 'none';
const bg = document.createElement('div');
bg.id = 'nx-modal-bg';
bg.onclick = (e) => { if (e.target === bg) closeModal(); };
bg.innerHTML = `
<div id="nx-modal">
<div class="nx-header">
<h3 id="nx-title">Segnalazioni</h3>
<button class="nx-close" onclick="document.getElementById('nx-modal-bg')?.remove();var f=document.getElementById('ai-chat-fab');if(f)f.style.display='flex'"></button>
</div>
<div class="nx-tabs">
<button class="nx-tab" data-tab="new">Nuova</button>
<button class="nx-tab" data-tab="mine">Le Mie</button>
<button class="nx-tab" data-tab="done">Risolte</button>
<button class="nx-tab" data-tab="notif">Notifiche</button>
</div>
<div class="nx-body" id="nx-body"></div>
</div>
`;
document.body.appendChild(bg);
// Tab click
bg.querySelectorAll('.nx-tab').forEach(t => {
t.onclick = () => switchTab(t.dataset.tab);
});
switchTab(tab || 'new');
}
function closeModal() {
document.getElementById('nx-modal-bg')?.remove();
// Rimostra FAB ARIA quando il modale segnalazioni si chiude
var aiaFab = document.getElementById('ai-chat-fab');
if (aiaFab) aiaFab.style.display = 'flex';
}
function switchTab(tab) {
document.querySelectorAll('.nx-tab').forEach(t => {
t.classList.toggle('active', t.dataset.tab === tab);
});
const body = document.getElementById('nx-body');
if (!body) return;
if (tab === 'new') {
if (_chatMode && !_supportSessionId) renderChatMode(body);
else if (_chatMode && _supportSessionId) renderChatUI(body);
else renderNewForm(body);
}
else if (tab === 'mine') renderMyTickets(body, 'ACTIVE');
else if (tab === 'done') renderMyTickets(body, 'DONE');
else if (tab === 'notif') renderNotifications(body);
}
// ==================== TAB: CHAT MODE (Pre-Ticket AI Dialog) ====================
async function renderChatMode(body) {
_chatMessages = [];
_supportSessionId = null;
body.innerHTML = '<div style="text-align:center;padding:30px;color:#9ca3af">Avvio assistente...</div>';
try {
const res = await fetch(`${API}/support-sessions`, {
method: 'POST',
headers: _headers(true),
body: JSON.stringify({ product: CFG.product, callerEmail: CFG.userEmail, callerName: CFG.userName })
});
const d = await res.json();
if (d.success) {
_supportSessionId = d.data.sessionId;
_chatMessages.push({ role: 'ai', text: d.data.welcomeMessage });
} else {
_chatMessages.push({ role: 'ai', text: 'Ciao! Descrivi il problema e ti aiuto a risolverlo.' });
}
} catch(e) {
_chatMessages.push({ role: 'ai', text: 'Ciao! Descrivi il problema e ti aiuto a risolverlo.' });
}
renderChatUI(body);
}
function renderChatUI(body) {
body.style.padding = '0';
body.innerHTML = `
<div class="nx-chat-area" id="nx-chat-area"></div>
<div class="nx-chat-actions">
<button class="resolve" onclick="window._nxChatResolve()"> Risolto, grazie!</button>
<button class="classic" onclick="window._nxChatSkip()">📝 Segnalazione classica</button>
</div>
<div class="nx-chat-input">
<input type="text" id="nx-chat-input" placeholder="Scrivi il tuo problema..." autofocus>
<button id="nx-chat-send" onclick="window._nxChatSend()"></button>
</div>
`;
// Render messages
const area = document.getElementById('nx-chat-area');
_chatMessages.forEach(m => appendChatBubble(area, m.role, m.text));
area.scrollTop = area.scrollHeight;
// Enter to send
document.getElementById('nx-chat-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !_chatLoading) window._nxChatSend();
});
}
function appendChatBubble(area, role, text) {
const div = document.createElement('div');
div.className = `nx-chat-msg ${role}`;
div.textContent = text;
area.appendChild(div);
}
function showTyping(area) {
const div = document.createElement('div');
div.className = 'nx-typing';
div.id = 'nx-typing';
div.innerHTML = '<span></span><span></span><span></span>';
area.appendChild(div);
area.scrollTop = area.scrollHeight;
}
function hideTyping() {
document.getElementById('nx-typing')?.remove();
}
window._nxChatSend = async () => {
const input = document.getElementById('nx-chat-input');
const text = input.value.trim();
if (!text || _chatLoading) return;
input.value = '';
_chatLoading = true;
const area = document.getElementById('nx-chat-area');
_chatMessages.push({ role: 'user', text });
appendChatBubble(area, 'user', text);
area.scrollTop = area.scrollHeight;
const sendBtn = document.getElementById('nx-chat-send');
sendBtn.disabled = true;
showTyping(area);
try {
const res = await fetch(`${API}/support-sessions/${_supportSessionId}/message`, {
method: 'POST',
headers: _headers(true),
body: JSON.stringify({ content: text })
});
const d = await res.json();
hideTyping();
if (d.success) {
const reply = d.data.reply;
_chatMessages.push({ role: 'ai', text: reply });
appendChatBubble(area, 'ai', reply);
// Check for escalation/forward action from AI tool calls
if (d.data.action) {
if (d.data.action.type === 'ESCALATED') {
const ticketId = d.data.action.data?.ticketId;
_chatMessages.push({ role: 'system', text: `Ticket #${ticketId} creato. Il team lo prendera in carico.` });
appendChatBubble(area, 'system', `Ticket #${ticketId} creato. Il team lo prendera in carico.`);
}
if (d.data.action.type === 'FORWARDED') {
const expertName = d.data.action.data?.expertName || 'esperto';
_chatMessages.push({ role: 'system', text: `Domanda inoltrata a ${expertName}. Riceverai una risposta via email.` });
appendChatBubble(area, 'system', `Domanda inoltrata a ${expertName}. Riceverai una risposta via email.`);
}
}
} else {
_chatMessages.push({ role: 'ai', text: 'Mi dispiace, non sono riuscito a elaborare la risposta. Puoi riprovare?' });
appendChatBubble(area, 'ai', 'Mi dispiace, non sono riuscito a elaborare la risposta. Puoi riprovare?');
}
} catch(e) {
hideTyping();
_chatMessages.push({ role: 'ai', text: 'Connessione non disponibile. Prova la segnalazione classica.' });
appendChatBubble(area, 'ai', 'Connessione non disponibile. Prova la segnalazione classica.');
}
area.scrollTop = area.scrollHeight;
_chatLoading = false;
sendBtn.disabled = false;
input.focus();
};
window._nxChatResolve = async () => {
if (_supportSessionId) {
try {
await fetch(`${API}/support-sessions/${_supportSessionId}/resolve`, {
method: 'POST',
headers: _headers(true),
body: JSON.stringify({ rating: 5 })
});
} catch(e) { /* silent */ }
}
const area = document.getElementById('nx-chat-area');
if (area) {
appendChatBubble(area, 'system', 'Grazie! Se hai altre domande, siamo qui. 😊');
area.scrollTop = area.scrollHeight;
}
// Reset after 2 seconds
setTimeout(() => { _supportSessionId = null; _chatMessages = []; _chatMode = true; closeModal(); }, 2000);
};
window._nxChatSkip = () => {
_chatMode = false;
const body = document.getElementById('nx-body');
if (body) {
body.style.padding = '16px 20px';
renderNewForm(body);
}
};
// ==================== TAB: NUOVA SEGNALAZIONE ====================
function renderNewForm(body) {
const ctx = getContext();
body.innerHTML = `
<div class="nx-ctx">📍 ${ctx.pageUrl.split('#')[1] || ctx.pageUrl.split('/').pop()} ${ctx.companyName ? '| 🏢 ' + ctx.companyName : ''}</div>
<div class="nx-row">
<select class="nx-select" id="nx-tipo" style="flex:1">
<option value="COMPLAINT">🐛 Bug / Errore</option>
<option value="SUPPORT_REQUEST">🎨 Interfaccia</option>
<option value="INFO_REQUEST">💡 Suggerimento</option>
<option value="OTHER"> Domanda</option>
</select>
<select class="nx-select" id="nx-prio" style="flex:1">
<option value="MEDIUM">Media</option>
<option value="LOW">Bassa</option>
<option value="HIGH">Alta</option>
<option value="URGENT">Urgente</option>
</select>
</div>
<textarea class="nx-textarea" id="nx-desc" placeholder="Descrivi il problema... (puoi incollare screenshot con Ctrl+V)"></textarea>
<div class="nx-att-list" id="nx-att-list"></div>
<div class="nx-row">
<button class="nx-btn" id="nx-voice-btn" onclick="window._nxVoice()">🎤 Parla</button>
<button class="nx-btn" id="nx-screenshot-btn" onclick="window._nxScreenshot()">📸 Screenshot</button>
<label class="nx-btn" style="cursor:pointer">
📎 Allega
<input type="file" accept="image/*,.pdf" multiple style="display:none" onchange="window._nxAttachFiles(this.files)">
</label>
</div>
<button class="nx-btn nx-btn-primary" id="nx-submit" onclick="window._nxSubmit()">Invia Segnalazione</button>
<div id="nx-result" style="display:none"></div>
`;
// Paste handler
const desc = document.getElementById('nx-desc');
desc.addEventListener('paste', handlePaste);
// Drag & drop
desc.addEventListener('dragover', (e) => { e.preventDefault(); desc.classList.add('drag-over'); });
desc.addEventListener('dragleave', () => desc.classList.remove('drag-over'));
desc.addEventListener('drop', (e) => {
e.preventDefault();
desc.classList.remove('drag-over');
if (e.dataTransfer.files.length) addFiles(e.dataTransfer.files);
});
}
// ==================== PASTE / SCREENSHOT / ATTACH ====================
function handlePaste(e) {
const items = e.clipboardData?.items;
if (!items) return;
for (const item of items) {
if (item.type.startsWith('image/')) {
e.preventDefault();
const blob = item.getAsFile();
blobToAttachment(blob, `screenshot_${Date.now()}.png`);
}
}
}
function blobToAttachment(blob, filename) {
const reader = new FileReader();
reader.onload = () => {
_attachments.push({
filename: filename,
mimeType: blob.type,
size: blob.size,
data: reader.result.split(',')[1], // base64 without prefix
preview: reader.result
});
renderAttachments();
};
reader.readAsDataURL(blob);
}
function addFiles(files) {
for (const file of files) {
if (_attachments.length >= 10) break;
blobToAttachment(file, file.name);
}
}
window._nxAttachFiles = (files) => addFiles(files);
window._nxScreenshot = async () => {
const btn = document.getElementById('nx-screenshot-btn');
try {
// Hide modal temporarily
const modal = document.getElementById('nx-modal-bg');
if (modal) modal.style.display = 'none';
btn.textContent = '⏳ Cattura...';
// Wait a frame for modal to hide
await new Promise(r => setTimeout(r, 100));
// Use html2canvas if available, otherwise use Screen Capture API
if (typeof html2canvas !== 'undefined') {
const canvas = await html2canvas(document.body, { useCORS: true, scale: 1 });
canvas.toBlob((blob) => {
if (blob) blobToAttachment(blob, `screenshot_${Date.now()}.png`);
if (modal) modal.style.display = 'flex';
btn.textContent = '📸 Screenshot';
}, 'image/png');
} else if (navigator.mediaDevices?.getDisplayMedia) {
const stream = await navigator.mediaDevices.getDisplayMedia({ video: { mediaSource: 'screen' } });
const track = stream.getVideoTracks()[0];
const imageCapture = new ImageCapture(track);
const bitmap = await imageCapture.grabFrame();
track.stop();
const canvas = document.createElement('canvas');
canvas.width = bitmap.width;
canvas.height = bitmap.height;
canvas.getContext('2d').drawImage(bitmap, 0, 0);
canvas.toBlob((blob) => {
if (blob) blobToAttachment(blob, `screenshot_${Date.now()}.png`);
if (modal) modal.style.display = 'flex';
btn.textContent = '📸 Screenshot';
}, 'image/png');
} else {
// Fallback: canvas from body (basic)
if (modal) modal.style.display = 'flex';
btn.textContent = '📸 Screenshot';
alert('Screenshot non supportato in questo browser. Usa Ctrl+V per incollare.');
}
} catch(err) {
const modal = document.getElementById('nx-modal-bg');
if (modal) modal.style.display = 'flex';
btn.textContent = '📸 Screenshot';
if (err.name !== 'NotAllowedError') {
console.warn('Screenshot error:', err);
}
}
};
function renderAttachments() {
const list = document.getElementById('nx-att-list');
if (!list) return;
list.innerHTML = _attachments.map((a, i) => `
<div class="nx-att-item">
<img src="${a.preview}" alt="${a.filename}">
<button class="nx-att-remove" onclick="window._nxRemoveAtt(${i})"></button>
</div>
`).join('');
}
window._nxRemoveAtt = (i) => { _attachments.splice(i, 1); renderAttachments(); };
// ==================== VOICE ====================
window._nxVoice = () => {
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
alert('Riconoscimento vocale non supportato. Usa Chrome.'); return;
}
const recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
recognition.lang = CFG.lang === 'it' ? 'it-IT' : CFG.lang + '-' + CFG.lang.toUpperCase();
recognition.continuous = false;
const btn = document.getElementById('nx-voice-btn');
btn.textContent = '🔴 Parla ora...';
btn.style.background = '#fee2e2';
recognition.onresult = (e) => {
const desc = document.getElementById('nx-desc');
desc.value += (desc.value ? '\n' : '') + e.results[0][0].transcript;
btn.textContent = '🎤 Parla'; btn.style.background = '';
};
recognition.onerror = recognition.onend = () => { btn.textContent = '🎤 Parla'; btn.style.background = ''; };
recognition.start();
};
// ==================== SUBMIT ====================
window._nxSubmit = async () => {
const desc = document.getElementById('nx-desc')?.value?.trim();
const tipo = document.getElementById('nx-tipo')?.value;
const prio = document.getElementById('nx-prio')?.value;
const submitBtn = document.getElementById('nx-submit');
const result = document.getElementById('nx-result');
if (!desc && _attachments.length === 0) { alert('Descrivi il problema o allega uno screenshot'); return; }
submitBtn.disabled = true;
submitBtn.textContent = 'Invio in corso...';
const ctx = getContext();
const versionTag = `[v${WIDGET_VERSION} build ${WIDGET_BUILD}]`;
const payload = {
transcription: desc ? `${desc}\n\n${versionTag}` : `Segnalazione con screenshot ${versionTag}`,
callerName: CFG.userName || ctx.userName,
callerEmail: CFG.userEmail || ctx.userEmail,
kind: tipo,
priority: prio,
queue: 'SUPPORT',
product: CFG.product,
pageUrl: ctx.pageUrl,
reporterRole: CFG.userRole || ctx.userRole,
lang: CFG.lang,
appVersion: WIDGET_VERSION,
appBuild: WIDGET_BUILD
};
try {
// 1. Create ticket
const res = await fetch(`${API}/tickets/voice-report`, {
method: 'POST',
headers: _headers(true),
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.success) {
const ticketId = data.data.ticket.id;
// 2. Attach files
if (_attachments.length > 0) {
await fetch(`${API}/tickets/${ticketId}/attachments-batch`, {
method: 'POST',
headers: _headers(true),
body: JSON.stringify({ attachments: _attachments.map(a => ({ filename: a.filename, mimeType: a.mimeType, size: a.size, data: a.data })) })
});
}
result.className = 'nx-result ok';
result.style.display = 'block';
result.innerHTML = `✅ Segnalazione #${ticketId} inviata!<br><small>${data.data.ticket.aiResponse || 'Grazie, il team la prendera in carico.'}</small>`;
submitBtn.textContent = '✓ Inviato';
// Play audio if available
if (data.data.voice?.audioBase64) {
try { new Audio(data.data.voice.audioBase64).play(); } catch(e) {}
}
} else {
throw new Error(data.error?.message || 'Errore');
}
} catch(err) {
result.className = 'nx-result err';
result.style.display = 'block';
result.textContent = '❌ ' + (err.message || 'Servizio non raggiungibile');
submitBtn.disabled = false;
submitBtn.textContent = 'Invia Segnalazione';
}
};
// ==================== TAB: MIE SEGNALAZIONI / RISOLTE ====================
async function renderMyTickets(body, statusFilter) {
body.innerHTML = '<div class="nx-empty">Caricamento...</div>';
const email = CFG.userEmail;
if (!email) { body.innerHTML = '<div class="nx-empty">Email utente non configurata</div>'; return; }
try {
const res = await fetch(`${API}/tickets/my?callerEmail=${encodeURIComponent(email)}&status=${statusFilter}&product=${CFG.product}`, {
headers: _headers()
});
const data = await res.json();
if (!data.success || !data.data.tickets.length) {
body.innerHTML = `<div class="nx-empty">${statusFilter === 'DONE' ? 'Nessuna segnalazione risolta' : 'Nessuna segnalazione aperta'}</div>`;
return;
}
body.innerHTML = data.data.tickets.map(t => `
<div class="nx-ticket" onclick="window._nxToggleTicket(this)">
<div class="nx-ticket-head">
<span class="nx-ticket-id">#${t.id}</span>
<span class="nx-ticket-status nx-status-${t.status}">${t.status.replace('_', ' ')}</span>
</div>
<div class="nx-ticket-subject">${t.subject || t.operativeSummary?.substring(0, 80) || 'Senza oggetto'}</div>
<div class="nx-ticket-meta">${t.priority} | ${timeAgo(t.createdAt)}</div>
${t.aiResponse ? `<div class="nx-ticket-ai" style="display:none">💬 ${t.aiResponse}</div>` : ''}
${t.messages?.length ? `<div class="nx-ticket-ai" style="display:none">📝 ${t.messages.map(m => `<b>${m.role}</b>: ${m.content}`).join('<br>')}</div>` : ''}
${t.status === 'RESOLVED' ? `<button class="nx-reopen-btn" onclick="event.stopPropagation(); window._nxReopenTicket(${t.id}, this)" style="margin-top:8px;width:100%;padding:8px;border:1px solid #ef4444;border-radius:8px;background:#fef2f2;color:#dc2626;font-size:13px;font-weight:600;cursor:pointer">Non risolto — Riapri</button>` : ''}
</div>
`).join('');
} catch(err) {
body.innerHTML = '<div class="nx-empty">Servizio non raggiungibile</div>';
}
}
window._nxToggleTicket = (el) => {
el.querySelectorAll('.nx-ticket-ai').forEach(d => {
d.style.display = d.style.display === 'none' ? 'block' : 'none';
});
};
window._nxReopenTicket = async (ticketId, btn) => {
btn.disabled = true;
btn.textContent = 'Riapertura...';
try {
const res = await fetch(`${API}/tickets/${ticketId}/status`, {
method: 'PATCH',
headers: _headers(true),
body: JSON.stringify({ status: 'OPEN', performedBy: CFG.userEmail || 'utente' })
});
const data = await res.json();
if (data.success) {
btn.textContent = 'Riaperto!';
btn.style.background = '#d1fae5';
btn.style.color = '#065f46';
btn.style.borderColor = '#10b981';
// Aggiunge un messaggio al ticket con la segnalazione dell'utente
await fetch(`${API}/tickets/${ticketId}/message`, {
method: 'POST',
headers: _headers(true),
body: JSON.stringify({ content: 'Riaperto dall\'utente: il problema non era risolto.', role: 'USER' })
});
setTimeout(() => renderTickets(btn.closest('.nx-body') || document.querySelector('.nx-body')), 1000);
} else {
btn.textContent = data.error?.message || 'Errore';
btn.style.background = '#fee2e2';
}
} catch(e) {
btn.textContent = 'Errore connessione';
}
};
// ==================== TAB: NOTIFICHE ====================
async function renderNotifications(body) {
body.innerHTML = '<div class="nx-empty">Caricamento...</div>';
try {
const res = await fetch(`${API}/notifications?product=${CFG.product}&limit=30`, {
headers: _headers()
});
const data = await res.json();
if (!data.success || !data.data.notifications.length) {
body.innerHTML = '<div class="nx-empty">Nessuna notifica</div>';
return;
}
let html = `<div style="text-align:right;margin-bottom:8px">
<button class="nx-btn" style="font-size:12px;padding:4px 10px" onclick="window._nxReadAll()">Segna tutte lette</button>
</div>`;
html += data.data.notifications.map(n => `
<div class="nx-notif ${n.readAt ? '' : 'unread'}" onclick="window._nxReadNotif(${n.id}, this)">
<div class="nx-notif-title"><span class="nx-notif-dot ${n.type}"></span>${n.title}</div>
${n.body ? `<div class="nx-notif-body">${n.body}</div>` : ''}
<div class="nx-notif-meta">${n.type} | ${timeAgo(n.createdAt)}</div>
</div>
`).join('');
body.innerHTML = html;
} catch(err) {
body.innerHTML = '<div class="nx-empty">Servizio non raggiungibile</div>';
}
}
window._nxReadNotif = async (id, el) => {
try {
await fetch(`${API}/notifications/${id}/read`, {
method: 'PUT', headers: _headers()
});
el.classList.remove('unread');
pollNotifications();
} catch(e) {}
};
window._nxReadAll = async () => {
try {
await fetch(`${API}/notifications/read-all?product=${CFG.product}`, {
method: 'PUT', headers: _headers()
});
document.querySelectorAll('.nx-notif.unread').forEach(el => el.classList.remove('unread'));
pollNotifications();
} catch(e) {}
};
// ==================== UTILS ====================
function getContext() {
return {
pageUrl: window.location.href,
userName: CFG.userName || (typeof api !== 'undefined' && api.user?.name) || '',
userEmail: CFG.userEmail || (typeof api !== 'undefined' && api.user?.email) || '',
userRole: CFG.userRole || (typeof api !== 'undefined' && api.user?.role) || '',
companyName: (typeof api !== 'undefined' && api.companies?.[0]?.name) || ''
};
}
function timeAgo(dateStr) {
const diff = (Date.now() - new Date(dateStr).getTime()) / 1000;
if (diff < 60) return 'ora';
if (diff < 3600) return Math.floor(diff / 60) + ' min fa';
if (diff < 86400) return Math.floor(diff / 3600) + ' ore fa';
return Math.floor(diff / 86400) + ' giorni fa';
}
// ==================== MAINTENANCE CHECK (polling maintenance.json) ====================
var _maintOverlay = null;
function checkMaintenance() {
fetch('/maintenance.json?_=' + Date.now()).then(function(r) { return r.json(); }).then(function(d) {
if (d && d.active === true) {
if (!_maintOverlay) {
_maintOverlay = document.createElement('div');
_maintOverlay.id = 'agile-maint-overlay';
_maintOverlay.style.cssText = 'position:fixed;inset:0;z-index:99999;background:rgba(10,11,15,0.95);display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif';
_maintOverlay.innerHTML = '<div style="text-align:center;max-width:400px;padding:32px">'
+ '<div style="font-size:48px;margin-bottom:16px">🔧</div>'
+ '<h2 style="color:#fff;font-size:22px;font-weight:700;margin:0 0 12px">Aggiornamento in corso</h2>'
+ '<p style="color:#9ca3af;font-size:15px;line-height:1.6;margin:0 0 20px">Stiamo applicando un miglioramento al sistema. Torneremo operativi tra pochi istanti.</p>'
+ '<div style="width:48px;height:48px;border:3px solid rgba(124,58,237,0.3);border-top-color:#7c3aed;border-radius:50%;margin:0 auto;animation:agile-spin 1s linear infinite"></div>'
+ '<style>@keyframes agile-spin{to{transform:rotate(360deg)}}</style>'
+ '</div>';
document.body.appendChild(_maintOverlay);
}
} else {
if (_maintOverlay) { _maintOverlay.remove(); _maintOverlay = null; }
}
}).catch(function() {
if (_maintOverlay) { _maintOverlay.remove(); _maintOverlay = null; }
});
}
setInterval(checkMaintenance, 10000);
setTimeout(checkMaintenance, 3000);
// ==================== START ====================
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

View File

@ -214,6 +214,7 @@ function loadSidebar() {
{ {
label: 'Sistema', i18nKey: 'nav.system', label: 'Sistema', i18nKey: 'nav.system',
items: [ items: [
{ name: 'Guida all\'uso', href: 'guida.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/></svg>` },
{ name: 'Impostazioni', href: 'settings.html', icon: iconCog(), i18nKey: 'nav.settings' }, { name: 'Impostazioni', href: 'settings.html', icon: iconCog(), i18nKey: 'nav.settings' },
{ name: 'Architettura', href: 'architecture.html', icon: iconCubeTransparent() }, { name: 'Architettura', href: 'architecture.html', icon: iconCubeTransparent() },
{ name: 'Simulazione Demo', href: 'simulate.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd"/></svg>` }, { name: 'Simulazione Demo', href: 'simulate.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd"/></svg>` },

View File

@ -904,6 +904,22 @@ const HelpSystem = (function () {
// ── Rendering del contenuto HTML di help ───────────────────────── // ── Rendering del contenuto HTML di help ─────────────────────────
// Mapping pageId -> ancora capitolo nella Guida completa (guida.html)
var _guideAnchor = {
'dashboard': 'cap-4',
'assessment': 'cap-5',
'risks': 'cap-6',
'incidents': 'cap-7',
'policies': 'cap-8',
'supply-chain': 'cap-9',
'training': 'cap-10',
'assets': 'cap-11',
'reports': 'cap-12',
'feedback': 'cap-13',
'workflow': 'cap-4',
'settings': 'cap-3'
};
function _renderHelpContent(pageId) { function _renderHelpContent(pageId) {
var data = _helpContent[pageId]; var data = _helpContent[pageId];
if (!data) return null; if (!data) return null;
@ -936,6 +952,15 @@ const HelpSystem = (function () {
html += '</div>'; html += '</div>';
} }
// Link alla guida completa (per utenti che vogliono approfondire)
var anchor = _guideAnchor[pageId] || '';
var guideHref = 'guida.html' + (anchor ? '#' + anchor : '');
html += '<div style="margin-top:20px;padding:14px 18px;background:#eff6ff;border-left:4px solid #1e40af;border-radius:6px;">';
html += '<strong style="color:#1e40af;">Sei nuovo?</strong> ';
html += 'Apri la <a href="' + guideHref + '" style="color:#1e40af;font-weight:600;">Guida all\'uso completa</a> ';
html += 'con spiegazioni in parole semplici, esempi pratici e cosa dice la norma.';
html += '</div>';
html += '</div>'; html += '</div>';
return { title: data.title, content: html }; return { title: data.title, content: html };

View File

@ -0,0 +1,189 @@
/*
* Mobile Conversion Layer Agile Technology suite
* Riutilizzabile su EViX, QSA, NIS2, TRPG, LG231, ecc.
* Si attiva SOLO su mobile (768px) desktop invariato.
* Classi prefissate .mobconv-* per zero conflitti con CSS host.
* */
/* ── Variabili di tema (override via inline style sul container) ── */
:root {
--mobconv-accent: #E31E24;
--mobconv-accent2: #B71518;
--mobconv-bg: #0A0A0A;
--mobconv-text: #FFFFFF;
--mobconv-sub: rgba(255,255,255,0.75);
--mobconv-border: rgba(255,255,255,0.12);
}
/* ── Sticky CTA bar (bottom) — visibile solo mobile ────────────── */
.mobconv-sticky {
display: none; /* nascosta di default */
position: fixed;
left: 0; right: 0;
bottom: 0;
z-index: 9990;
background: linear-gradient(180deg, rgba(10,10,10,0.96), rgba(10,10,10,1));
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
border-top: 1px solid var(--mobconv-border);
padding: 10px 16px;
padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
box-shadow: 0 -8px 32px rgba(0,0,0,0.45);
transform: translateY(100%);
transition: transform .28s ease;
}
.mobconv-sticky-row { max-width: 540px; margin: 0 auto; }
.mobconv-cta { max-width: 100%; }
/* iPhone XS / SE: padding ridotto per non sovrastare */
@media (max-width: 380px) {
.mobconv-sticky { padding: 8px 12px; padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px)); }
.mobconv-cta { font-size: 14px !important; padding: 0 12px !important; min-height: 44px !important; }
.mobconv-pill { min-height: 44px !important; min-width: 44px !important; }
}
.mobconv-sticky.show { transform: translateY(0); }
.mobconv-sticky-row {
display: flex;
gap: 8px;
align-items: stretch;
}
.mobconv-pill {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 48px;
min-height: 48px;
padding: 0 12px;
border-radius: 14px;
background: rgba(255,255,255,0.08);
color: var(--mobconv-text);
border: 1px solid var(--mobconv-border);
text-decoration: none;
font-size: 18px;
transition: background .15s, transform .15s;
}
.mobconv-pill:active { transform: scale(0.96); background: rgba(255,255,255,0.14); }
.mobconv-pill.wa { background: rgba(37,211,102,0.18); border-color: rgba(37,211,102,0.45); color: #25D366; }
.mobconv-cta {
flex: 1 1 auto;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 48px;
padding: 0 16px;
border-radius: 14px;
background: linear-gradient(135deg, var(--mobconv-accent), var(--mobconv-accent2));
color: #FFFFFF;
text-decoration: none;
font-weight: 700;
font-size: 14.5px;
letter-spacing: 0.01em;
box-shadow: 0 8px 24px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.06) inset;
transition: transform .15s;
}
.mobconv-cta:active { transform: scale(0.98); }
/* ── WhatsApp FAB (alternativa standalone — usabile su Diamante-like) ── */
.mobconv-wa-fab {
display: none;
position: fixed;
right: 16px;
bottom: calc(20px + env(safe-area-inset-bottom, 0px));
z-index: 9980;
width: 56px;
height: 56px;
border-radius: 50%;
background: #25D366;
color: white;
align-items: center;
justify-content: center;
text-decoration: none;
font-size: 26px;
box-shadow: 0 8px 28px rgba(37,211,102,0.45);
transition: transform .2s, box-shadow .2s;
animation: mobconv-pulse 2.4s ease-out infinite;
}
.mobconv-wa-fab:active { transform: scale(0.92); }
@keyframes mobconv-pulse {
0% { box-shadow: 0 8px 28px rgba(37,211,102,0.45), 0 0 0 0 rgba(37,211,102,0.55); }
70% { box-shadow: 0 8px 28px rgba(37,211,102,0.45), 0 0 0 18px rgba(37,211,102,0); }
100% { box-shadow: 0 8px 28px rgba(37,211,102,0.45), 0 0 0 0 rgba(37,211,102,0); }
}
/* ── Trust ribbon (sotto hero su mobile, opzionale) ─────────────── */
.mobconv-trust {
display: none;
background: linear-gradient(180deg, rgba(0,0,0,0.04), rgba(0,0,0,0.02));
border-top: 1px solid rgba(0,0,0,0.06);
border-bottom: 1px solid rgba(0,0,0,0.06);
padding: 14px 12px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.mobconv-trust::-webkit-scrollbar { display: none; }
.mobconv-trust-row {
display: inline-flex;
align-items: center;
gap: 18px;
white-space: nowrap;
padding: 0 4px;
}
.mobconv-trust-item {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12.5px;
font-weight: 600;
color: rgba(0,0,0,0.7);
letter-spacing: 0.01em;
}
.mobconv-trust-item::before {
content: "✓";
color: var(--mobconv-accent);
font-weight: 900;
font-size: 11px;
}
.mobconv-trust-item:not(:first-child)::after {
content: "·";
color: rgba(0,0,0,0.25);
margin-left: 18px;
font-size: 16px;
font-weight: 900;
}
/* Variante dark per siti dark-theme */
.mobconv-trust.dark {
background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02));
border-top-color: rgba(255,255,255,0.08);
border-bottom-color: rgba(255,255,255,0.08);
}
.mobconv-trust.dark .mobconv-trust-item { color: rgba(255,255,255,0.85); }
.mobconv-trust.dark .mobconv-trust-item:not(:first-child)::after { color: rgba(255,255,255,0.2); }
/* ── Mobile-only: attiva tutto sotto i 768px ─────────────────────── */
@media (max-width: 768px) {
.mobconv-sticky { display: block; }
.mobconv-wa-fab[data-enabled="true"] { display: inline-flex; }
/* Hide WA FAB quando sticky è visibile (ridondante) */
.mobconv-sticky.show ~ .mobconv-wa-fab { display: none !important; }
.mobconv-trust { display: block; }
/* iOS: no zoom su input/select (font-size ≥16px) */
input, select, textarea {
font-size: max(16px, 1rem) !important;
}
/* Spazio extra in bottom della pagina per non coprire CTA esistenti */
body.mobconv-padded {
padding-bottom: calc(96px + env(safe-area-inset-bottom, 0px)) !important;
}
}
/* Su desktop nasconde TUTTO senza margine d'errore */
@media (min-width: 769px) {
.mobconv-sticky, .mobconv-wa-fab, .mobconv-trust { display: none !important; }
}

192
public/mobile-conversion.js Normal file
View File

@ -0,0 +1,192 @@
/*
* Mobile Conversion Layer Agile Technology suite
* Versione 1.0 2026-05-18
*
* Si attiva solo su mobile (768px).
* Configurabile via <script data-*>:
*
* <script src="mobile-conversion.js"
* data-primary-text="Prenota demo"
* data-primary-action="agilehub-open"
* data-primary-href="#contact"
* data-phone="+390000000000"
* data-whatsapp="390000000000"
* data-wa-message="Buongiorno, vorrei info su…"
* data-show-wa-fab="false"
* data-trust-items='["12 moduli","4 anni","AI"]'
* data-trust-after="section.hero"
* data-trust-theme="dark"
* data-accent="#E31E24"
* data-accent2="#B71518"
* data-scroll-trigger="400"
* defer></script>
*
* data-primary-action:
* - "agilehub-open" apre la modale AgileHubApplet (se presente)
* - "anchor" smooth scroll a data-primary-href
* - "external" location.assign(data-primary-href)
* */
(function () {
'use strict';
/* ── Config dal <script data-*> ── */
var script = document.currentScript || (function () {
var s = document.getElementsByTagName('script');
for (var i = s.length - 1; i >= 0; i--) {
if (s[i].src && s[i].src.indexOf('mobile-conversion') > -1) return s[i];
}
return s[s.length - 1];
})();
var ds = script.dataset || {};
var CFG = {
primaryText: ds.primaryText || 'Contattaci',
primaryAction: ds.primaryAction || 'anchor',
primaryHref: ds.primaryHref || '#contact',
primaryIcon: ds.primaryIcon || '→',
phone: ds.phone || '',
whatsapp: ds.whatsapp || '',
waMessage: ds.waMessage || '',
showWaFab: ds.showWaFab === 'true',
trustItems: ds.trustItems || null, // JSON array
trustAfter: ds.trustAfter || null, // CSS selector
trustTheme: ds.trustTheme || 'light', // 'light' | 'dark'
accent: ds.accent || '#E31E24',
accent2: ds.accent2 || '#B71518',
scrollTrigger: parseInt(ds.scrollTrigger || '400', 10)
};
/* ── Bail-out se desktop o se siamo in un iframe ── */
if (window.innerWidth > 768) return;
/* ── Inject CSS variables override (se accent custom) ── */
if (CFG.accent !== '#E31E24' || CFG.accent2 !== '#B71518') {
var st = document.createElement('style');
st.textContent = ':root{--mobconv-accent:'+CFG.accent+';--mobconv-accent2:'+CFG.accent2+'}';
document.head.appendChild(st);
}
/* ── Parse trust items ── */
var trustList = null;
if (CFG.trustItems) {
try { trustList = JSON.parse(CFG.trustItems); } catch (e) { trustList = null; }
}
/* ── Costruisci WhatsApp URL ── */
function waUrl() {
if (!CFG.whatsapp) return null;
var msg = CFG.waMessage ? '?text=' + encodeURIComponent(CFG.waMessage) : '';
return 'https://wa.me/' + CFG.whatsapp + msg;
}
/* ── Render sticky bar ── */
function buildSticky() {
var html = ['<div class="mobconv-sticky" id="mobconv-sticky"><div class="mobconv-sticky-row">'];
if (CFG.phone) {
html.push('<a class="mobconv-pill" href="tel:' + CFG.phone.replace(/\s/g,'') + '" aria-label="Telefono">📞</a>');
}
var wa = waUrl();
if (wa) {
html.push('<a class="mobconv-pill wa" href="' + wa + '" target="_blank" rel="noopener" aria-label="WhatsApp">💬</a>');
}
// Primary CTA
var ctaAttrs = 'class="mobconv-cta" id="mobconv-primary"';
if (CFG.primaryAction === 'agilehub-open') {
ctaAttrs += ' href="#" data-agilehub-open';
} else if (CFG.primaryAction === 'external') {
ctaAttrs += ' href="' + CFG.primaryHref + '" target="_blank" rel="noopener"';
} else {
ctaAttrs += ' href="' + CFG.primaryHref + '"';
}
html.push('<a ' + ctaAttrs + '>' + CFG.primaryText + ' <span style="opacity:.85">' + CFG.primaryIcon + '</span></a>');
html.push('</div></div>');
return html.join('');
}
/* ── Render WhatsApp FAB ── */
function buildFab() {
if (!CFG.showWaFab || !CFG.whatsapp) return '';
return '<a class="mobconv-wa-fab" data-enabled="true" href="' + waUrl() + '" target="_blank" rel="noopener" aria-label="Scrivici su WhatsApp">' +
'<i class="fab fa-whatsapp"></i>' +
'</a>';
}
/* ── Render trust ribbon ── */
function buildTrust() {
if (!trustList || !trustList.length) return null;
var dark = CFG.trustTheme === 'dark' ? ' dark' : '';
var items = trustList.map(function (t) {
return '<span class="mobconv-trust-item">' + t + '</span>';
}).join('');
return '<div class="mobconv-trust' + dark + '"><div class="mobconv-trust-row">' + items + '</div></div>';
}
/* ── Mount on DOM ready ── */
function mount() {
// Inject sticky bar at end of body
var stickyHtml = buildSticky();
var fabHtml = buildFab();
var wrap = document.createElement('div');
wrap.innerHTML = stickyHtml + fabHtml;
while (wrap.firstChild) document.body.appendChild(wrap.firstChild);
// Inject trust ribbon after hero (or specified selector)
var trustHtml = buildTrust();
if (trustHtml && CFG.trustAfter) {
var anchor = document.querySelector(CFG.trustAfter);
if (anchor) {
var t = document.createElement('div');
t.innerHTML = trustHtml;
anchor.parentNode.insertBefore(t.firstChild, anchor.nextSibling);
}
}
// Padding body bottom per non coprire CTA esistenti in fondo pagina
document.body.classList.add('mobconv-padded');
// Smooth scroll per anchor primary
if (CFG.primaryAction === 'anchor') {
var btn = document.getElementById('mobconv-primary');
if (btn) {
btn.addEventListener('click', function (e) {
var href = btn.getAttribute('href');
if (href && href.charAt(0) === '#') {
var tgt = document.querySelector(href);
if (tgt) {
e.preventDefault();
tgt.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
});
}
}
// agilehub-open: il modulo agilehub-applet.js auto-binda [data-agilehub-open]
// quindi nessun bind manuale serve qui.
// Scroll listener: show sticky bar dopo X px
var stickyEl = document.getElementById('mobconv-sticky');
var triggered = false;
function onScroll() {
var y = window.scrollY || document.documentElement.scrollTop;
if (!triggered && y >= CFG.scrollTrigger) {
stickyEl.classList.add('show');
triggered = true;
} else if (triggered && y < CFG.scrollTrigger - 60) {
// Reset solo se scrolla MOLTO in alto (-60 hysteresis)
stickyEl.classList.remove('show');
triggered = false;
}
}
window.addEventListener('scroll', onScroll, { passive: true });
onScroll(); // check iniziale
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', mount);
} else {
mount();
}
})();

View File

@ -479,7 +479,7 @@
try { try {
const result = await api.listRisks(params); const result = await api.listRisks(params);
if (result.success) { if (result.success) {
risksData = result.data || []; risksData = (result.data && result.data.items) || [];
renderRisksTable(risksData); renderRisksTable(risksData);
} else { } else {
showNotification(result.message || 'Errore nel caricamento rischi', 'error'); showNotification(result.message || 'Errore nel caricamento rischi', 'error');

View File

@ -0,0 +1,209 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Standard Continuita di Servizio — NIS2 Agile</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Segoe UI',-apple-system,sans-serif;font-size:14.5px;line-height:1.7;color:#1A1A2E;background:#F5F7FA}
.page{max-width:960px;margin:0 auto;background:#fff;box-shadow:0 4px 40px rgba(0,0,0,.08)}
.doc-header{background:linear-gradient(135deg,#0D1B2A 0%,#1565C0 100%);color:#fff;padding:40px 48px}
.header-top{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap;margin-bottom:22px}
.brand{display:flex;align-items:center;gap:10px}
.brand-icon{width:44px;height:44px;border-radius:10px;background:rgba(255,255,255,.12);display:flex;align-items:center;justify-content:center;font-size:1.2rem;color:rgba(255,255,255,.8)}
.brand-name{font-size:1rem;font-weight:800}
.brand-sub{font-size:.68rem;opacity:.5}
.doc-meta{text-align:right;font-size:.75rem;opacity:.5}
.doc-meta strong{display:block;color:rgba(255,255,255,.8);opacity:1;font-size:.85rem}
.doc-title{font-size:1.65rem;font-weight:800;letter-spacing:-.02em;line-height:1.2;margin-bottom:6px}
.doc-title span{color:rgba(255,255,255,.8)}
.doc-sub{font-size:.84rem;opacity:.6}
.tag-row{display:flex;gap:8px;flex-wrap:wrap;margin-top:18px}
.tag{display:inline-flex;align-items:center;gap:5px;padding:4px 12px;border-radius:20px;font-size:.7rem;font-weight:600;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.18);color:rgba(255,255,255,.85)}
.content{padding:36px 48px}
h2{font-size:1.25rem;font-weight:800;color:#1565C0;margin:36px 0 14px;padding-bottom:6px;border-bottom:2px solid #eee}
h2:first-child{margin-top:0}
h2 i{margin-right:8px}
h3{font-size:1rem;font-weight:700;color:#1565C0;margin:22px 0 8px}
p{margin:8px 0}ul,ol{margin:8px 0 8px 20px}li{margin:4px 0}
.info-box{background:#E8F5E9;border-left:4px solid #2E7D32;padding:14px 18px;border-radius:0 8px 8px 0;margin:16px 0;font-size:.88rem}
.info-box i{color:#2E7D32;margin-right:6px}
.warn-box{background:#FFF3E0;border-left:4px solid #F57F17;padding:14px 18px;border-radius:0 8px 8px 0;margin:16px 0;font-size:.88rem}
.warn-box i{color:#F57F17;margin-right:6px}
table{width:100%;border-collapse:collapse;margin:14px 0;font-size:.82rem}
th{background:#f5f5f5;padding:10px 12px;text-align:left;font-weight:700;border-bottom:2px solid #ddd;font-size:.75rem;text-transform:uppercase;letter-spacing:.5px;color:#333}
td{padding:9px 12px;border-bottom:1px solid #F0F2F5;vertical-align:top}
tr:hover td{background:#FAFBFC}
code{background:#f5f5f5;padding:2px 6px;border-radius:4px;font-size:.8rem;font-family:'Courier New',monospace;color:#333}
pre{background:#1A1A2E;color:#E0E4E8;padding:16px 20px;border-radius:8px;overflow-x:auto;font-size:.78rem;line-height:1.5;margin:12px 0}
.layer{display:grid;grid-template-columns:60px 1fr;gap:0;margin:16px 0}
.layer-num{background:#1565C0;color:#fff;font-weight:800;font-size:1.1rem;display:flex;align-items:center;justify-content:center;border-radius:8px 0 0 8px}
.layer-body{background:#f9f9f9;padding:16px 20px;border-radius:0 8px 8px 0;border:1px solid #eee;border-left:none}
.layer-body h4{margin:0 0 6px;color:#1565C0;font-size:.95rem}
.badge-ok{display:inline-block;background:#2E7D32;color:#fff;padding:2px 8px;border-radius:10px;font-size:.68rem;font-weight:700}
.badge-warn{display:inline-block;background:#F57F17;color:#fff;padding:2px 8px;border-radius:10px;font-size:.68rem;font-weight:700}
.badge-crit{display:inline-block;background:#C62828;color:#fff;padding:2px 8px;border-radius:10px;font-size:.68rem;font-weight:700}
.doc-footer{background:#f5f5f5;padding:24px 48px;border-top:1px solid #ddd;font-size:.78rem;color:#5F6B7A;display:flex;justify-content:space-between;flex-wrap:wrap;gap:12px}
@media print{body{background:#fff}.page{box-shadow:none}}
@media(max-width:700px){.content,.doc-header,.doc-footer{padding:20px}.layer{grid-template-columns:1fr}.layer-num{border-radius:8px 8px 0 0;padding:8px}.layer-body{border-radius:0 0 8px 8px;border-left:1px solid #eee}}
</style>
</head>
<body>
<div class="page">
<div class="doc-header">
<div class="header-top">
<div class="brand">
<div class="brand-icon"><i class="fas fa-shield-halved"></i></div>
<div><div class="brand-name">NIS2 Agile</div><div class="brand-sub">Cybersecurity Compliance NIS2</div></div>
</div>
<div class="doc-meta"><strong>Standard Operativo</strong>Continuita di Servizio</div>
</div>
<div class="doc-title">Strategia di <span>Continuita di Servizio</span></div>
<div class="doc-sub">Architettura di resilienza, monitoraggio, auto-recovery e protezione dei dati</div>
<div class="tag-row">
<span class="tag"><i class="fas fa-calendar"></i> Aprile 2026</span>
<span class="tag"><i class="fas fa-microchip"></i> PHP+MySQL+Qdrant</span>
<span class="tag"><i class="fas fa-server"></i> Hetzner CPX31</span>
<span class="tag"><i class="fas fa-heartbeat"></i> 99.9% SLA Target</span>
</div>
</div>
<div class="content">
<h2><i class="fas fa-sitemap"></i> 1. Architettura di Servizio</h2>
<p><strong>NIS2 Agile</strong> e composto dai seguenti servizi gestiti sulla piattaforma Hetzner condivisa con gli altri prodotti della suite Agile Technology.</p>
<table><tr><th>Servizio</th><th>Stack</th><th>Healthcheck</th></tr><tr><td><code>nis2-app</code></td><td>PHP+MySQL+Qdrant</td><td><span class="badge-ok">/health</span></td></tr><tr><td><code>MySQL/PostgreSQL</code></td><td>Database condiviso</td><td><span class="badge-ok">ping</span></td></tr><tr><td><code>Qdrant</code></td><td>Vector DB (embeddings)</td><td><span class="badge-ok">HTTP 200</span></td></tr><tr><td><code>AgileHub (NEXUS)</code></td><td>Ticket + AI + Notifiche</td><td><span class="badge-ok">/health</span></td></tr></table>
<div class="info-box">
<i class="fas fa-info-circle"></i>
<strong>Deploy a caldo</strong>: le modifiche al codice sorgente vengono applicate via bind mount Docker o PM2 reload. Nessun rebuild necessario per aggiornamenti ordinari.
</div>
<h2><i class="fas fa-layer-group"></i> 2. Strategia di Resilienza a 7 Livelli</h2>
<div class="layer"><div class="layer-num">L1</div><div class="layer-body">
<h4><i class="fas fa-rotate"></i> Auto-Restart Container / PM2</h4>
<p>Ogni servizio ha <code>restart: unless-stopped</code> (Docker) o <code>pm2 --watch</code> (Node.js). Se un processo crasha, viene riavviato automaticamente entro 10-30 secondi.</p>
<p><strong>Tempo di recovery</strong>: &lt; 30 secondi</p>
</div></div>
<div class="layer"><div class="layer-num">L2</div><div class="layer-body">
<h4><i class="fas fa-heartbeat"></i> Healthcheck Nativo</h4>
<p>Ogni servizio espone un endpoint <code>/health</code> o <code>/api/health</code> verificato ogni 30 secondi. Se il check fallisce 3 volte consecutive, il container/processo viene ricreato automaticamente.</p>
<p><strong>Rileva</strong>: crash applicativo, memoria esaurita, deadlock, loop infinito.</p>
</div></div>
<div class="layer"><div class="layer-num">L3</div><div class="layer-body">
<h4><i class="fas fa-database"></i> Database Retry con Reconnect</h4>
<p>Tutti i servizi implementano retry automatico su errori transitori del database (MySQL/PostgreSQL):</p>
<ul>
<li><strong>2006</strong> — Server has gone away (reconnect)</li>
<li><strong>2013</strong> — Lost connection during query (retry)</li>
<li><strong>1213</strong> — Deadlock found (retry con backoff)</li>
</ul>
<p>Strategia: 2 retry con backoff esponenziale + reconnect automatico.</p>
</div></div>
<div class="layer"><div class="layer-num">L4</div><div class="layer-body">
<h4><i class="fas fa-cloud"></i> Resilienza API Esterne</h4>
<p>Le chiamate ad API esterne (Claude AI, ElevenLabs TTS, Voyage embeddings) hanno protezione a 3 livelli:</p>
<ol>
<li><strong>Retry con backoff</strong>: 3 tentativi su errori transitori (429, 500, 502, 503)</li>
<li><strong>Degradazione graceful</strong>: se l'AI non risponde, il prodotto funziona con i dati locali</li>
<li><strong>Frontend fallback</strong>: messaggio "AI temporaneamente non disponibile" con suggerimenti alternativi</li>
</ol>
</div></div>
<div class="layer"><div class="layer-num">L5</div><div class="layer-body">
<h4><i class="fas fa-bell"></i> Error Alerting in Tempo Reale</h4>
<p>Ogni errore critico genera una notifica AgileHub (campanella) per l'admin e, se configurato, un'email di alert. L'admin vede l'errore entro 30 secondi via polling badge.</p>
</div></div>
<div class="layer"><div class="layer-num">L6</div><div class="layer-body">
<h4><i class="fas fa-eye"></i> Monitoraggio Esterno AgileHub</h4>
<p>Il cron AgileHub (<code>ticket-agent-cron.sh</code>) verifica lo stato di tutti i container ogni 2 minuti. Se un prodotto non e raggiungibile, viene segnalato nel log e inviata notifica.</p>
<p>Complementare ai healthcheck interni: rileva problemi di rete, proxy Apache, DNS.</p>
</div></div>
<div class="layer"><div class="layer-num">L7</div><div class="layer-body">
<h4><i class="fas fa-broom"></i> Manutenzione Preventiva</h4>
<p>Script automatici prevengono il degrado:</p>
<table>
<tr><th>Operazione</th><th>Frequenza</th><th>Funzione</th></tr>
<tr><td>Disk monitor</td><td>Ogni 6 ore</td><td>Alert email se disco &gt; 85%</td></tr>
<tr><td>Cleanup</td><td>Settimanale</td><td>Elimina file temporanei, Docker prune</td></tr>
<tr><td>SSL renewal</td><td>Settimanale</td><td>Rinnovo automatico Let's Encrypt</td></tr>
<tr><td>Claude auth</td><td>Orario</td><td>Refresh token OAuth Claude Code</td></tr>
</table>
</div></div>
<h2><i class="fas fa-shield-halved"></i> 3. Protezione da Abuso</h2>
<h3>Rate Limiting</h3>
<table>
<tr><th>Endpoint</th><th>Limite</th><th>Finestra</th><th>Tipo</th></tr>
<tr><td>Login</td><td>5 tentativi</td><td>15 min</td><td>Per IP + lockout</td></tr>
<tr><td>API generiche</td><td>100 req</td><td>1 ora</td><td>Per chiave/utente</td></tr>
<tr><td>Password reset</td><td>3 req</td><td>1 ora</td><td>Per email</td></tr>
<tr><td>Webhook esterni</td><td>50 req</td><td>1 min</td><td>Per IP</td></tr>
</table>
<h2><i class="fas fa-wrench"></i> 4. Modalita Manutenzione</h2>
<p>Quando il sistema AgileHub applica un fix approvato, il <strong>maintenance mode</strong> si attiva automaticamente:</p>
<ol>
<li>Il cron agent crea il flag <code>/tmp/maintenance-NIS2.flag</code></li>
<li>Apache rileva il flag e mostra la pagina di manutenzione a tutti gli utenti</li>
<li>Il fix viene applicato (Claude nel container DevEnv)</li>
<li>Il flag viene rimosso — il prodotto torna online</li>
<li>La pagina di manutenzione si auto-aggiorna e reindirizza gli utenti</li>
</ol>
<div class="warn-box">
<i class="fas fa-exclamation-triangle"></i>
<strong>Nessun intervento manuale richiesto</strong>: il maintenance mode e completamente automatico. L'utente vede un messaggio professionale e il sistema torna online da solo.
</div>
<h2><i class="fas fa-exclamation-triangle"></i> 5. Matrice Rischi e Mitigazione</h2>
<table>
<tr><th>Scenario</th><th>Probabilita</th><th>Impatto</th><th>Mitigazione</th><th>Recovery</th></tr>
<tr><td>Crash singolo servizio</td><td><span class="badge-warn">Media</span></td><td><span class="badge-warn">Medio</span></td><td>Auto-restart Docker/PM2</td><td>&lt; 30s</td></tr>
<tr><td>Database restart</td><td><span class="badge-ok">Bassa</span></td><td><span class="badge-crit">Alto</span></td><td>DB retry + reconnect</td><td>&lt; 5s</td></tr>
<tr><td>AI non disponibile</td><td><span class="badge-warn">Media</span></td><td><span class="badge-ok">Basso</span></td><td>3 retry + graceful</td><td>Funzioni base intatte</td></tr>
<tr><td>Disco pieno</td><td><span class="badge-ok">Bassa</span></td><td><span class="badge-crit">Alto</span></td><td>Monitor 6h + cleanup</td><td>Alert prima del 85%</td></tr>
<tr><td>SSL scaduto</td><td><span class="badge-ok">Bassa</span></td><td><span class="badge-crit">Alto</span></td><td>Auto-renewal certbot</td><td>Rinnovo 30gg prima</td></tr>
<tr><td>Attacco brute force</td><td><span class="badge-warn">Media</span></td><td><span class="badge-warn">Medio</span></td><td>Rate limit + lockout</td><td>Blocco 15 min</td></tr>
<tr><td>Errore deployment</td><td><span class="badge-warn">Media</span></td><td><span class="badge-crit">Alto</span></td><td>Maintenance mode + git revert</td><td>&lt; 2 min rollback</td></tr>
</table>
<h2><i class="fas fa-handshake"></i> 6. Service Level Agreement</h2>
<table>
<tr><th>Metrica</th><th>Target</th><th>Misurazione</th></tr>
<tr><td>Uptime servizio</td><td><strong>99.9%</strong> (8.7h downtime/anno)</td><td>Health monitor + log</td></tr>
<tr><td>Recovery da crash</td><td>&lt; 30 secondi</td><td>Docker restart + healthcheck</td></tr>
<tr><td>Recovery da aggiornamento</td><td>&lt; 5 minuti</td><td>Maintenance mode + deploy</td></tr>
<tr><td>Rilevamento downtime</td><td>&lt; 5 minuti</td><td>Cron monitor + email alert</td></tr>
<tr><td>Tempo risposta API (p95)</td><td>&lt; 500ms</td><td>Health endpoint response time</td></tr>
</table>
<h2><i class="fas fa-clipboard-check"></i> 7. Checklist Operativa</h2>
<h3>Verifica automatica (quotidiana)</h3>
<ul>
<li>Tutti i container/processi in stato healthy/online</li>
<li>Campanella AgileHub — nessuna notifica di errore sistema</li>
<li>Email — nessun alert da health monitor</li>
</ul>
<h3>Prima di un aggiornamento manuale</h3>
<ol>
<li>Verificare che non ci siano utenti attivi (o attivare maintenance mode)</li>
<li>Eseguire l'aggiornamento</li>
<li>Verificare tutti i servizi: <code>docker compose ps</code> o <code>pm2 list</code></li>
<li>Controllare i log per errori</li>
</ol>
</div>
<div class="doc-footer">
<div>&copy; 2026 NIS2 Agile &mdash; <a href="https://nis2.agile.software" style="color:#1565C0">nis2.agile.software</a></div>
<div>Standard Continuita di Servizio v1.0 &bull; Aprile 2026 &bull; Agile Technology SRL</div>
</div>
</div>
</body>
</html>