[UI] SWAP V2: Bootstrap Italia ora UFFICIALE su 27 pagine app (kill della vecchia UI)

Promosse le pagine -bi a nomi ufficiali (contenuto BI), link -bi->plain, titoli '(BI)' rimossi.
La vecchia UI delle pagine app e sostituita. Backup: git (commit 36967f9) + /tmp/nis2-v1-backup.

Promosse (27): auth (login/register/onboarding/forgot/reset) + app (dashboard/assessment/
risks/incidents/policies/supply-chain/training/assets/reports/settings/isms/acn-gap/normative/
whistleblowing/kb/cross-analysis/service-continuity/guida/companies) + admin (index/orgs/users).
common-bi.js: sidebar BI ora linka le pagine ufficiali.

ESCLUSA: index.html (landing marketing 1298 righe) -> conversione dedicata a parte (la -bi
era 557 righe, rischio perdita contenuti). Pagine marketing/demo (presentation/simulate/
workflow/architecture/index-en) restano vecchio stile (fuori scope app autenticata).

Verifica produzione: 27/27 servite 200 con BI, 0 link -bi residui, 0 '(BI)' nei titoli,
auth intatta (401 su pwd errata), common-bi.js node --check OK.

Rollback: git revert HEAD  (oppure ripristino da /tmp/nis2-v1-backup).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-06-12 14:49:15 +02:00
co-authored by Claude Opus 4.8
parent 36967f9452
commit 1c64211685
28 changed files with 2474 additions and 3105 deletions
+65 -39
View File
@@ -4,8 +4,11 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Cross-Analysis — NIS2 Agile</title>
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati
via JS (chat/portfolio/storico) -> parita' funzionale assoluta. BI veste la chrome. -->
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<link rel="stylesheet" href="/css/style.css">
<style>
:root {
--nis2-cyan: #06B6D4;
@@ -14,6 +17,10 @@
--l4-purple-bg: #f5f3ff;
}
/* Icone sprite BI: dimensione coerente inline */
.ico { width: 1em; height: 1em; vertical-align: -0.125em; fill: currentColor; }
.ico-lg { width: 2.5rem; height: 2.5rem; }
/* ── Chat container ─────────────────────────── */
.chat-wrap {
display: flex;
@@ -70,6 +77,7 @@
}
.btn-send:hover:not(:disabled) { background: #6d28d9; }
.btn-send:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-send:focus-visible { outline: 3px solid #fff; outline-offset: 2px; box-shadow: 0 0 0 5px var(--l4-purple); }
/* ── Message bubbles ────────────────────────── */
.msg {
@@ -215,6 +223,7 @@
transition: background 0.2s;
}
.quick-q:hover { background: #ede9fe; }
.quick-q:focus-visible { outline: 2px solid var(--l4-purple); outline-offset: 2px; }
/* ── Privacy badge ──────────────────────────── */
.privacy-badge {
@@ -240,7 +249,7 @@
text-align: center;
gap: 10px;
}
.chat-empty i { font-size: 2.5rem; color: #c4b5fd; }
.chat-empty .ico { color: #c4b5fd; }
.chat-empty h4 { color: var(--gray-600); font-size: 0.95rem; }
.chat-empty p { font-size: 0.82rem; max-width: 360px; }
@@ -259,6 +268,7 @@
transition: color 0.2s, border-color 0.2s;
}
.tab-btn.active { color: var(--l4-purple); border-bottom-color: var(--l4-purple); }
.tab-btn:focus-visible { outline: 2px solid var(--l4-purple); outline-offset: -2px; }
.tab-pane { display: none; }
.tab-pane.active { display: block; }
</style>
@@ -269,15 +279,15 @@
<main class="main-content">
<header class="content-header">
<h2>
<i class="fa-solid fa-brain" style="color:var(--l4-purple);margin-right:8px;"></i>
<svg class="ico" aria-hidden="true" style="color:var(--l4-purple);margin-right:8px;"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-software"></use></svg>
AI Cross-Analysis
<span class="privacy-badge" style="margin-left:10px;">
<i class="fa-solid fa-lock"></i> Dati aggregati — privacy garantita
<svg class="ico" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-lock"></use></svg> Dati aggregati — privacy garantita
</span>
</h2>
<div class="content-header-actions">
<span class="badge" style="background:var(--l4-purple-bg);color:var(--l4-purple);font-size:0.75rem;padding:4px 10px;border-radius:12px;">
<i class="fa-solid fa-user-tie"></i> Solo Consulenti &amp; Admin
<svg class="ico" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-user"></use></svg> Solo Consulenti &amp; Admin
</span>
</div>
</header>
@@ -294,24 +304,24 @@
<!-- Tabs -->
<div class="card" style="padding:0;overflow:hidden;">
<div class="tab-bar">
<button class="tab-btn active" onclick="switchTab('chat')">
<i class="fa-solid fa-message"></i> Chat AI
<div class="tab-bar" role="tablist" aria-label="Sezioni AI Cross-Analysis">
<button class="tab-btn active" onclick="switchTab('chat')" role="tab" aria-selected="true" aria-controls="tab-chat">
<svg class="ico" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-comment"></use></svg> Chat AI
</button>
<button class="tab-btn" onclick="switchTab('history')">
<i class="fa-solid fa-clock-rotate-left"></i> Storico
<button class="tab-btn" onclick="switchTab('history')" role="tab" aria-selected="false" aria-controls="tab-history">
<svg class="ico" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-clock"></use></svg> Storico
</button>
<button class="tab-btn" onclick="switchTab('portfolio')">
<i class="fa-solid fa-chart-pie"></i> Portfolio dati
<button class="tab-btn" onclick="switchTab('portfolio')" role="tab" aria-selected="false" aria-controls="tab-portfolio">
<svg class="ico" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-chart-line"></use></svg> Portfolio dati
</button>
</div>
<!-- TAB: CHAT -->
<div class="tab-pane active" id="tab-chat">
<div class="tab-pane active" id="tab-chat" role="tabpanel">
<div class="chat-wrap">
<div class="chat-messages" id="chat-messages">
<div class="chat-messages" id="chat-messages" aria-live="polite">
<div class="chat-empty" id="chat-empty">
<i class="fa-solid fa-brain"></i>
<svg class="ico ico-lg" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-software"></use></svg>
<h4>Analisi AI del tuo portfolio clienti</h4>
<p>Fai una domanda sui dati aggregati delle tue organizzazioni. I dati vengono trasmessi in forma anonima e aggregata.</p>
</div>
@@ -326,14 +336,15 @@
<button class="quick-q" onclick="setQuestion('Benchmark: dove si posiziona il mio portfolio rispetto agli standard NIS2?')">Benchmark NIS2</button>
</div>
<div class="chat-input-row">
<label for="chat-input" class="visually-hidden">Domanda all'AI sul portfolio</label>
<textarea id="chat-input" placeholder="Es: Quali sono le aree più critiche nel mio portfolio clienti?" rows="2"
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMessage();}"></textarea>
<button class="btn-send" onclick="sendMessage()" id="btn-send">
<i class="fa-solid fa-paper-plane"></i>
<button class="btn-send" onclick="sendMessage()" id="btn-send" aria-label="Invia domanda">
<svg class="ico" aria-hidden="true" style="color:#fff;"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-mail"></use></svg>
</button>
</div>
<div style="font-size:0.7rem;color:var(--gray-400);margin-top:6px;text-align:center;">
<i class="fa-solid fa-shield-halved"></i>
<svg class="ico" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-locked"></use></svg>
Dati anonimizzati e aggregati — nessun nome organizzazione inviato all'AI
</div>
</div>
@@ -341,14 +352,14 @@
</div>
<!-- TAB: HISTORY -->
<div class="tab-pane" id="tab-history">
<div class="tab-pane" id="tab-history" role="tabpanel">
<div style="padding:16px;" id="history-content">
<div class="spinner" style="margin:30px auto;display:block;"></div>
</div>
</div>
<!-- TAB: PORTFOLIO DATI -->
<div class="tab-pane" id="tab-portfolio">
<div class="tab-pane" id="tab-portfolio" role="tabpanel">
<div style="padding:16px;" id="portfolio-content">
<div class="spinner" style="margin:30px auto;display:block;"></div>
</div>
@@ -358,10 +369,23 @@
</main>
</div>
<script src="js/api.js"></script>
<script src="js/common.js"></script>
<script src="js/i18n.js"></script>
<script src="/js/api.js"></script>
<script src="/js/common.js"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
riusando tutti gli helper di common.js (zero backend). -->
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script>
if (window.bootstrap && bootstrap.loadFonts) {
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
}
</script>
<script src="/js/common-bi.js"></script>
<script src="/js/i18n.js"></script>
<script>
const SPRITE = '/vendor/bootstrap-italia/dist/svg/sprites.svg';
function ico(name, cls) { return `<svg class="ico ${cls||''}" aria-hidden="true"><use href="${SPRITE}#${name}"></use></svg>`; }
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar();
I18n.init();
@@ -378,12 +402,12 @@ async function loadPortfolio() {
if (r.error_code === 'INSUFFICIENT_ROLE') {
document.getElementById('portfolio-header').innerHTML = `
<div style="color:white;text-align:center;width:100%;">
<i class="fa-solid fa-lock" style="font-size:1.5rem;opacity:0.6;"></i><br>
${ico('it-lock','ico-lg')}<br>
<strong>Funzione riservata a Consulenti e Super Admin</strong>
</div>`;
document.querySelector('.card').innerHTML = `
<div style="padding:40px;text-align:center;color:var(--gray-500);">
<i class="fa-solid fa-lock" style="font-size:2rem;color:var(--gray-300);"></i>
${ico('it-lock','ico-lg')}
<p style="margin-top:12px;">Hai bisogno del ruolo <strong>Consultant</strong> o <strong>Super Admin</strong> per accedere all\'analisi cross-organizzazione.</p>
</div>`;
return;
@@ -481,9 +505,11 @@ function renderPortfolioTab(d) {
// ── Tab switching ─────────────────────────────────────────────────
function switchTab(tab) {
const tabs = ['chat','history','portfolio'];
document.querySelectorAll('.tab-btn').forEach((b, i) => {
const tabs = ['chat','history','portfolio'];
b.classList.toggle('active', tabs[i] === tab);
const active = tabs[i] === tab;
b.classList.toggle('active', active);
b.setAttribute('aria-selected', active ? 'true' : 'false');
});
document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
document.getElementById('tab-' + tab).classList.add('active');
@@ -535,10 +561,10 @@ async function sendMessage() {
function appendMessage(role, text) {
const msgs = document.getElementById('chat-messages');
const time = new Date().toLocaleTimeString('it-IT', {hour:'2-digit',minute:'2-digit'});
const icon = role === 'user' ? 'fa-user' : 'fa-brain';
const iconName = role === 'user' ? 'it-user' : 'it-software';
msgs.innerHTML += `
<div class="msg ${role}">
<div class="msg-avatar"><i class="fa-solid ${icon}"></i></div>
<div class="msg-avatar">${ico(iconName)}</div>
<div class="msg-body">
<div class="msg-bubble">${escapeHtml(text)}</div>
<div class="msg-time">${time}</div>
@@ -555,34 +581,34 @@ function appendAIResponse(result, orgCount) {
if (result.key_findings?.length) {
inner += `<div class="ai-section">
<div class="ai-section-title"><i class="fa-solid fa-magnifying-glass"></i> Risultati chiave</div>
<div class="ai-section-title">${ico('it-search')} Risultati chiave</div>
<ul>${result.key_findings.map(f => `<li>${escapeHtml(f)}</li>`).join('')}</ul>
</div>`;
}
if (result.recommendations?.length) {
inner += `<div class="ai-section">
<div class="ai-section-title"><i class="fa-solid fa-lightbulb"></i> Raccomandazioni</div>
<div class="ai-section-title">${ico('it-note')} Raccomandazioni</div>
<ul>${result.recommendations.map(r => `<li>${escapeHtml(r)}</li>`).join('')}</ul>
</div>`;
}
if (result.risk_areas?.length) {
inner += `<div class="ai-section">
<div class="ai-section-title"><i class="fa-solid fa-triangle-exclamation"></i> Aree di attenzione</div>
<div class="ai-section-title">${ico('it-warning-circle')} Aree di attenzione</div>
<ul>${result.risk_areas.map(r => `<li>${escapeHtml(r)}</li>`).join('')}</ul>
</div>`;
}
if (result.benchmark_note) {
inner += `<div class="ai-benchmark"><i class="fa-solid fa-chart-bar"></i> ${escapeHtml(result.benchmark_note)}</div>`;
inner += `<div class="ai-benchmark">${ico('it-chart-line')} ${escapeHtml(result.benchmark_note)}</div>`;
}
if (result.privacy_note) {
inner += `<div class="ai-privacy-note"><i class="fa-solid fa-lock"></i> ${escapeHtml(result.privacy_note)}</div>`;
inner += `<div class="ai-privacy-note">${ico('it-lock')} ${escapeHtml(result.privacy_note)}</div>`;
}
inner += `<div style="margin-top:8px;font-size:0.68rem;color:var(--gray-400);">Basato su ${orgCount} organizzazioni anonimizzate</div>`;
msgs.innerHTML += `
<div class="msg ai">
<div class="msg-avatar"><i class="fa-solid fa-brain"></i></div>
<div class="msg-avatar">${ico('it-software')}</div>
<div class="msg-body">
<div class="msg-bubble">${inner}</div>
<div class="msg-time">${time}</div>
@@ -595,10 +621,10 @@ function appendError(msg) {
const msgs = document.getElementById('chat-messages');
msgs.innerHTML += `
<div class="msg ai">
<div class="msg-avatar" style="background:var(--danger);"><i class="fa-solid fa-xmark"></i></div>
<div class="msg-avatar" style="background:var(--danger);">${ico('it-close')}</div>
<div class="msg-body">
<div class="msg-bubble" style="background:var(--danger-bg);color:var(--danger);">
<i class="fa-solid fa-triangle-exclamation"></i> ${escapeHtml(msg)}
${ico('it-warning-circle')} ${escapeHtml(msg)}
</div>
</div>
</div>`;
@@ -609,7 +635,7 @@ function appendThinking(id) {
const msgs = document.getElementById('chat-messages');
msgs.innerHTML += `
<div class="msg ai" id="${id}">
<div class="msg-avatar"><i class="fa-solid fa-brain"></i></div>
<div class="msg-avatar">${ico('it-software')}</div>
<div class="msg-body">
<div class="thinking-bubble">
<div class="thinking-dot"></div>