[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:
co-authored by
Claude Opus 4.8
parent
36967f9452
commit
1c64211685
+65
-39
@@ -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 & Admin
|
||||
<svg class="ico" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-user"></use></svg> Solo Consulenti & 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>
|
||||
|
||||
Reference in New Issue
Block a user