[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
+37 -12
View File
@@ -4,8 +4,15 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gap Analysis ACN - NIS2 Agile</title>
<link rel="stylesheet" href="css/style.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 (griglia misure/requisiti, scoring) -> parita' funzionale assoluta.
BI veste la chrome (sidebar, font, bottoni, icone sprite). -->
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<link rel="stylesheet" href="/css/style.css">
<style>
/* Stili specifici della pagina ACN — identici alla live acn-gap.html
(variabili --primary/--gray-* del design system esistente) */
.acn-intro { background:#eff6ff; border-left:4px solid var(--primary, #2563eb); padding:14px 18px; border-radius:8px; margin-bottom:20px; font-size:.92rem; line-height:1.6; }
.acn-level-badge { display:inline-block; padding:4px 12px; border-radius:999px; font-size:.8rem; font-weight:700; }
.acn-level-important { background:#fef3c7; color:#92400e; }
@@ -24,6 +31,7 @@
.acn-req-idx { color:var(--gray-400,#9ca3af); font-weight:700; margin-right:6px; }
.acn-opts { display:flex; flex-wrap:wrap; gap:6px; }
.acn-opt { min-height:38px; padding:6px 12px; border:1.5px solid var(--gray-200,#e5e7eb); border-radius:20px; background:#fff; cursor:pointer; font-size:.84rem; font-weight:600; }
.acn-opt:focus-visible { outline:2px solid var(--primary,#2563eb); outline-offset:2px; }
.acn-opt.sel-implemented { background:#16a34a; color:#fff; border-color:#16a34a; }
.acn-opt.sel-partial { background:#f59e0b; color:#fff; border-color:#f59e0b; }
.acn-opt.sel-not_implemented { background:#dc2626; color:#fff; border-color:#dc2626; }
@@ -34,6 +42,8 @@
.acn-func-score { display:flex; justify-content:space-between; padding:8px 0; border-bottom:1px solid var(--gray-100,#f3f4f6); font-size:.9rem; }
.acn-score-pill { font-weight:700; padding:2px 10px; border-radius:6px; }
.savehint { font-size:.78rem; color:var(--gray-400,#9ca3af); min-height:16px; }
/* icona inline nei bottoni BI */
.btn .icon { fill:currentColor; vertical-align:-2px; margin-right:6px; }
</style>
</head>
<body>
@@ -41,9 +51,10 @@
<aside class="sidebar" id="sidebar"></aside>
<main class="main-content">
<header class="content-header">
<h2 data-i18n="acn.title">Gap Analysis ACN (misure di base)</h2>
<h1 class="h2" data-i18n="acn.title">Gap Analysis ACN (misure di base)</h1>
<div class="content-header-actions">
<button class="btn btn-outline btn-sm" id="btn-ai-analyze" style="display:none;" onclick="acnAiAnalyze()" data-i18n="acn.ai">Analisi AI dei gap</button>
<button class="btn btn-outline btn-sm" id="btn-ai-analyze" style="display:none;" onclick="acnAiAnalyze()" data-i18n="acn.ai">
<svg class="icon icon-sm" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-star-full"></use></svg>Analisi AI dei gap</button>
</div>
</header>
@@ -58,9 +69,11 @@
<div class="card-body" style="padding:32px; text-align:center;">
<div id="acn-level-info" class="mb-24"></div>
<div id="acn-existing" class="mb-24" style="display:none; text-align:left;"></div>
<button class="btn btn-primary btn-lg" id="btn-new-acn" onclick="createAcn()" data-i18n="acn.new">Nuova Gap Analysis ACN</button>
<button class="btn btn-primary btn-lg" id="btn-new-acn" onclick="createAcn()" data-i18n="acn.new">
<svg class="icon icon-sm" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-plus"></use></svg>Nuova Gap Analysis ACN</button>
<div style="margin-top:14px;">
<button class="btn btn-outline btn-sm" onclick="showCatalog()" data-i18n="acn.catalog.btn">Consulta il catalogo dei requisiti</button>
<button class="btn btn-outline btn-sm" onclick="showCatalog()" data-i18n="acn.catalog.btn">
<svg class="icon icon-sm" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-search"></use></svg>Consulta il catalogo dei requisiti</button>
</div>
</div>
</div>
@@ -81,7 +94,7 @@
</div>
<div class="acn-bar"><div class="acn-bar-fill" id="acn-bar-fill"></div></div>
<div style="font-size:.82rem; color:var(--gray-500,#6b7280);"><span id="acn-answered">0</span> / <span id="acn-total">0</span> <span data-i18n="acn.answered">requisiti con risposta</span></div>
<div class="savehint" id="acn-savehint"></div>
<div class="savehint" id="acn-savehint" role="status" aria-live="polite"></div>
</div>
</div>
<div class="card mb-24">
@@ -90,7 +103,8 @@
<div class="card mb-24">
<div class="card-body" style="text-align:center;">
<div id="acn-wizard-msg"></div>
<button class="btn btn-primary btn-lg" id="btn-acn-complete" onclick="completeAcn()" data-i18n="acn.complete">Calcola punteggio e completa</button>
<button class="btn btn-primary btn-lg" id="btn-acn-complete" onclick="completeAcn()" data-i18n="acn.complete">
<svg class="icon icon-sm" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-chart-line"></use></svg>Calcola punteggio e completa</button>
</div>
</div>
</div>
@@ -117,17 +131,28 @@
<div class="card-body" id="acn-gaps"></div>
</div>
<div style="text-align:center; margin-bottom:24px;">
<button class="btn btn-outline" onclick="location.reload()" data-i18n="acn.backstart">Torna all'inizio</button>
<button class="btn btn-outline" onclick="location.reload()" data-i18n="acn.backstart">
<svg class="icon icon-sm" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-arrow-left"></use></svg>Torna all'inizio</button>
</div>
</div>
</div>
</main>
</div>
<script src="js/i18n.js"></script>
<script src="js/common.js"></script>
<script src="js/api.js"></script>
<script src="js/help.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 src="/js/help.js"></script>
<script>
'use strict';
const FUNC_LABELS = {
+61 -40
View File
@@ -4,6 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pannello Amministrazione - NIS2 Agile</title>
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css
cosi' style.css resta autorevole per i widget renderizzati via JS -> parita' funzionale.
Pagina in /admin/: gli asset usano path ASSOLUTI (/vendor, /js, /css) serviti dal web root. -->
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.admin-stats-grid {
@@ -55,6 +59,8 @@
.admin-stat-icon.orange { background: var(--warning-bg); color: #d97706; }
.admin-stat-icon.red { background: var(--danger-bg); color: var(--danger); }
.admin-stat-icon.purple { background: #f3e8ff; color: #7c3aed; }
/* Le icone BI usano fill=currentColor: garantiamo che il colore del wrapper sia ereditato. */
.admin-stat-icon svg.icon { fill: currentColor; width: 20px; height: 20px; }
.chart-container {
padding: 24px;
}
@@ -118,6 +124,10 @@
transform: translateY(-2px);
box-shadow: var(--card-shadow-hover);
}
.quick-link-card:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
.quick-link-icon {
width: 48px;
height: 48px;
@@ -132,6 +142,7 @@
.quick-link-icon svg {
width: 24px;
height: 24px;
fill: currentColor;
}
.quick-link-info h4 {
font-size: 0.9375rem;
@@ -178,7 +189,7 @@
<main class="main-content">
<header class="content-header">
<h2>Pannello Amministrazione</h2>
<h1 class="h2">Pannello Amministrazione</h1>
<div class="content-header-actions">
<span class="badge badge-danger">Super Admin</span>
</div>
@@ -188,64 +199,64 @@
<!-- Stats Row 1 -->
<div class="admin-stats-grid" id="stats-row-1">
<div class="admin-stat-card">
<div class="admin-stat-icon blue">
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M4 4a2 2 0 012-2h8a2 2 0 012 2v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"/></svg>
<div class="admin-stat-icon blue" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-pa"></use></svg>
</div>
<div class="admin-stat-label">Totale Organizzazioni</div>
<div class="admin-stat-value" id="stat-total-orgs">--</div>
<div class="admin-stat-label" id="lbl-total-orgs">Totale Organizzazioni</div>
<div class="admin-stat-value" id="stat-total-orgs" aria-labelledby="lbl-total-orgs">--</div>
</div>
<div class="admin-stat-card">
<div class="admin-stat-icon green">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>
<div class="admin-stat-icon green" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-check-circle"></use></svg>
</div>
<div class="admin-stat-label">Organizzazioni Attive</div>
<div class="admin-stat-value" id="stat-active-orgs">--</div>
<div class="admin-stat-label" id="lbl-active-orgs">Organizzazioni Attive</div>
<div class="admin-stat-value" id="stat-active-orgs" aria-labelledby="lbl-active-orgs">--</div>
</div>
<div class="admin-stat-card">
<div class="admin-stat-icon blue">
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z"/></svg>
<div class="admin-stat-icon blue" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-user"></use></svg>
</div>
<div class="admin-stat-label">Totale Utenti</div>
<div class="admin-stat-value" id="stat-total-users">--</div>
<div class="admin-stat-label" id="lbl-total-users">Totale Utenti</div>
<div class="admin-stat-value" id="stat-total-users" aria-labelledby="lbl-total-users">--</div>
</div>
<div class="admin-stat-card">
<div class="admin-stat-icon green">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"/></svg>
<div class="admin-stat-icon green" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-user"></use></svg>
</div>
<div class="admin-stat-label">Utenti Attivi</div>
<div class="admin-stat-value" id="stat-active-users">--</div>
<div class="admin-stat-label" id="lbl-active-users">Utenti Attivi</div>
<div class="admin-stat-value" id="stat-active-users" aria-labelledby="lbl-active-users">--</div>
</div>
</div>
<!-- Stats Row 2 -->
<div class="admin-stats-grid" id="stats-row-2">
<div class="admin-stat-card">
<div class="admin-stat-icon purple">
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"/><path fill-rule="evenodd" d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm9.707 5.707a1 1 0 00-1.414-1.414L9 12.586l-1.293-1.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>
<div class="admin-stat-icon purple" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-check"></use></svg>
</div>
<div class="admin-stat-label">Assessment Completati</div>
<div class="admin-stat-value" id="stat-assessments">--</div>
<div class="admin-stat-label" id="lbl-assessments">Assessment Completati</div>
<div class="admin-stat-value" id="stat-assessments" aria-labelledby="lbl-assessments">--</div>
</div>
<div class="admin-stat-card">
<div class="admin-stat-icon orange">
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z"/></svg>
<div class="admin-stat-icon orange" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-warning-circle"></use></svg>
</div>
<div class="admin-stat-label">Incidenti Aperti</div>
<div class="admin-stat-value" id="stat-incidents">--</div>
<div class="admin-stat-label" id="lbl-incidents">Incidenti Aperti</div>
<div class="admin-stat-value" id="stat-incidents" aria-labelledby="lbl-incidents">--</div>
</div>
<div class="admin-stat-card">
<div class="admin-stat-icon red">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 1.944A11.954 11.954 0 012.166 5C2.056 5.649 2 6.319 2 7c0 5.225 3.34 9.67 8 11.317C14.66 16.67 18 12.225 18 7c0-.682-.057-1.35-.166-2.001A11.954 11.954 0 0110 1.944zM11 14a1 1 0 11-2 0 1 1 0 012 0zm0-7a1 1 0 10-2 0v3a1 1 0 102 0V7z" clip-rule="evenodd"/></svg>
<div class="admin-stat-icon red" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-error"></use></svg>
</div>
<div class="admin-stat-label">Totale Rischi</div>
<div class="admin-stat-value" id="stat-risks">--</div>
<div class="admin-stat-label" id="lbl-risks">Totale Rischi</div>
<div class="admin-stat-value" id="stat-risks" aria-labelledby="lbl-risks">--</div>
</div>
<div class="admin-stat-card">
<div class="admin-stat-icon purple">
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zm7-10a1 1 0 01.967.744L14.146 7.2 17.5 8.512a1 1 0 010 1.836l-3.354 1.311-1.18 4.456a1 1 0 01-1.932 0L9.854 11.66 6.5 10.348a1 1 0 010-1.836l3.354-1.311 1.18-4.456A1 1 0 0112 2z"/></svg>
<div class="admin-stat-icon purple" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-software"></use></svg>
</div>
<div class="admin-stat-label">Interazioni AI</div>
<div class="admin-stat-value" id="stat-ai">--</div>
<div class="admin-stat-label" id="lbl-ai">Interazioni AI</div>
<div class="admin-stat-value" id="stat-ai" aria-labelledby="lbl-ai">--</div>
</div>
</div>
@@ -255,7 +266,7 @@
<h3>Distribuzione Piani</h3>
</div>
<div class="chart-container">
<div class="bar-chart" id="plans-chart">
<div class="bar-chart" id="plans-chart" role="img" aria-label="Grafico a barre: distribuzione delle organizzazioni per piano di sottoscrizione">
<div class="spinner" style="margin:40px auto;"></div>
</div>
</div>
@@ -267,10 +278,10 @@
<h3>Gestione Piattaforma</h3>
</div>
<div class="card-body">
<div class="quick-links-grid">
<nav class="quick-links-grid" aria-label="Collegamenti rapidi gestione piattaforma">
<a href="/admin/organizations.html" class="quick-link-card">
<div class="quick-link-icon">
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M4 4a2 2 0 012-2h8a2 2 0 012 2v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"/></svg>
<div class="quick-link-icon" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-pa"></use></svg>
</div>
<div class="quick-link-info">
<h4>Gestisci Organizzazioni</h4>
@@ -278,15 +289,15 @@
</div>
</a>
<a href="/admin/users.html" class="quick-link-card">
<div class="quick-link-icon">
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z"/></svg>
<div class="quick-link-icon" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-user"></use></svg>
</div>
<div class="quick-link-info">
<h4>Gestisci Utenti</h4>
<p>Visualizza e gestisci tutti gli utenti registrati sulla piattaforma.</p>
</div>
</a>
</div>
</nav>
</div>
</div>
</div>
@@ -295,6 +306,16 @@
<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>
// ── Auth check ───────────────────────────────────────────
+38 -22
View File
@@ -4,7 +4,12 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gestione Organizzazioni - NIS2 Agile</title>
<link rel="stylesheet" href="/css/style.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 (tabella org/paginazione/badge) -> parita' funzionale assoluta. BI veste la chrome.
Path relativi ../ perche' la pagina vive sotto public/admin/. -->
<link rel="stylesheet" href="../vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<link rel="stylesheet" href="../css/style.css">
<style>
.entity-type-badge {
display: inline-flex;
@@ -89,10 +94,11 @@
.admin-breadcrumb a:hover {
text-decoration: underline;
}
.admin-breadcrumb svg {
.admin-breadcrumb .icon {
width: 14px;
height: 14px;
color: var(--gray-400);
fill: currentColor;
}
</style>
</head>
@@ -102,7 +108,7 @@
<main class="main-content">
<header class="content-header">
<h2>Gestione Organizzazioni</h2>
<h1 class="h2">Gestione Organizzazioni</h1>
<div class="content-header-actions">
<span class="badge badge-danger">Super Admin</span>
</div>
@@ -110,17 +116,17 @@
<div class="content-body">
<!-- Breadcrumb -->
<div class="admin-breadcrumb">
<a href="/admin/index.html">Amministrazione</a>
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/></svg>
<span>Organizzazioni</span>
</div>
<nav class="admin-breadcrumb" aria-label="Percorso di navigazione">
<a href="../admin/index.html">Amministrazione</a>
<svg class="icon icon-sm" aria-hidden="true"><use href="../vendor/bootstrap-italia/dist/svg/sprites.svg#it-chevron-right"></use></svg>
<span aria-current="page">Organizzazioni</span>
</nav>
<!-- Table -->
<div class="card">
<div class="card-body" style="padding:0;">
<div id="orgs-container">
<div class="spinner" style="margin:60px auto;"></div>
<div class="spinner" style="margin:60px auto;" role="status" aria-label="Caricamento in corso"></div>
</div>
</div>
</div>
@@ -128,9 +134,19 @@
</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>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
@@ -176,7 +192,7 @@
// ── Caricamento ──────────────────────────────────────────
async function loadOrganizations(page) {
const container = document.getElementById('orgs-container');
container.innerHTML = '<div class="spinner" style="margin:60px auto;"></div>';
container.innerHTML = '<div class="spinner" style="margin:60px auto;" role="status" aria-label="Caricamento in corso"></div>';
currentPage = page;
try {
@@ -201,7 +217,7 @@
if (!orgs || orgs.length === 0) {
container.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M4 4a2 2 0 012-2h8a2 2 0 012 2v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"/></svg>
<svg class="icon" aria-hidden="true" style="width:48px;height:48px;fill:var(--gray-300);"><use href="../vendor/bootstrap-italia/dist/svg/sprites.svg#it-pa"></use></svg>
<h4>Nessuna organizzazione trovata</h4>
<p>Le organizzazioni registrate appariranno qui.</p>
</div>`;
@@ -213,14 +229,14 @@
<table>
<thead>
<tr>
<th>Nome</th>
<th>Settore</th>
<th>Tipo Entita'</th>
<th>Membri</th>
<th>Ultimo Score</th>
<th>Piano</th>
<th>Stato</th>
<th>Data Creazione</th>
<th scope="col">Nome</th>
<th scope="col">Settore</th>
<th scope="col">Tipo Entita'</th>
<th scope="col">Membri</th>
<th scope="col">Ultimo Score</th>
<th scope="col">Piano</th>
<th scope="col">Stato</th>
<th scope="col">Data Creazione</th>
</tr>
</thead>
<tbody>`;
+50 -31
View File
@@ -4,7 +4,12 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gestione Utenti - NIS2 Agile</title>
<link rel="stylesheet" href="/css/style.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 (tabella utenti/azioni) -> parita' funzionale assoluta. BI veste la chrome.
Path ../ perche' questa pagina vive in /admin/. -->
<link rel="stylesheet" href="../vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<link rel="stylesheet" href="../css/style.css">
<style>
.role-badge {
display: inline-flex;
@@ -59,6 +64,11 @@
.active-toggle input:checked + .toggle-slider::after {
transform: translateX(16px);
}
/* Focus visibile WCAG su toggle keyboard-operabile */
.active-toggle input:focus-visible + .toggle-slider {
outline: 2px solid var(--primary, #06c);
outline-offset: 2px;
}
.pagination {
display: flex;
align-items: center;
@@ -109,9 +119,7 @@
.admin-breadcrumb a:hover {
text-decoration: underline;
}
.admin-breadcrumb svg {
width: 14px;
height: 14px;
.admin-breadcrumb .icon {
color: var(--gray-400);
}
</style>
@@ -122,7 +130,7 @@
<main class="main-content">
<header class="content-header">
<h2>Gestione Utenti</h2>
<h1 class="h2">Gestione Utenti</h1>
<div class="content-header-actions">
<span class="badge badge-danger">Super Admin</span>
</div>
@@ -130,17 +138,17 @@
<div class="content-body">
<!-- Breadcrumb -->
<div class="admin-breadcrumb">
<a href="/admin/index.html">Amministrazione</a>
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/></svg>
<span>Utenti</span>
</div>
<nav class="admin-breadcrumb" aria-label="Percorso di navigazione">
<a href="../admin/index.html">Amministrazione</a>
<svg class="icon icon-sm" aria-hidden="true"><use href="../vendor/bootstrap-italia/dist/svg/sprites.svg#it-chevron-right"></use></svg>
<span aria-current="page">Utenti</span>
</nav>
<!-- Table -->
<div class="card">
<div class="card-body" style="padding:0;">
<div id="users-container">
<div class="spinner" style="margin:60px auto;"></div>
<div id="users-container" aria-live="polite">
<div class="spinner" style="margin:60px auto;" role="status" aria-label="Caricamento utenti"></div>
</div>
</div>
</div>
@@ -148,9 +156,19 @@
</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>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
@@ -165,12 +183,12 @@
const me = await api.getMe();
if (!me.success || !me.data || me.data.role !== 'super_admin') {
showNotification('Accesso non autorizzato.', 'error');
setTimeout(() => { window.location.href = '/dashboard.html'; }, 1500);
setTimeout(() => { window.location.href = '../dashboard.html'; }, 1500);
return;
}
loadUsers(1);
} catch (e) {
window.location.href = '/dashboard.html';
window.location.href = '../dashboard.html';
}
})();
@@ -187,7 +205,7 @@
// ── Caricamento ──────────────────────────────────────────
async function loadUsers(page) {
const container = document.getElementById('users-container');
container.innerHTML = '<div class="spinner" style="margin:60px auto;"></div>';
container.innerHTML = '<div class="spinner" style="margin:60px auto;" role="status" aria-label="Caricamento utenti"></div>';
currentPage = page;
try {
@@ -212,7 +230,7 @@
if (!users || users.length === 0) {
container.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z"/></svg>
<svg class="icon icon-lg" aria-hidden="true" style="margin:0 auto;"><use href="../vendor/bootstrap-italia/dist/svg/sprites.svg#it-user"></use></svg>
<h4>Nessun utente trovato</h4>
<p>Gli utenti registrati appariranno qui.</p>
</div>`;
@@ -224,14 +242,14 @@
<table>
<thead>
<tr>
<th>Nome</th>
<th>Email</th>
<th>Ruolo</th>
<th>Organizzazioni</th>
<th>Ultimo Login</th>
<th>Attivo</th>
<th>Data Creazione</th>
<th>Azioni</th>
<th scope="col">Nome</th>
<th scope="col">Email</th>
<th scope="col">Ruolo</th>
<th scope="col">Organizzazioni</th>
<th scope="col">Ultimo Login</th>
<th scope="col">Attivo</th>
<th scope="col">Data Creazione</th>
<th scope="col">Azioni</th>
</tr>
</thead>
<tbody>`;
@@ -266,15 +284,16 @@
<td>${lastLogin}</td>
<td>
<label class="active-toggle" title="${isActive ? 'Attivo' : 'Inattivo'}">
<input type="checkbox" ${isActive ? 'checked' : ''} onchange="toggleUserActive(${user.id}, this.checked)" ${role === 'super_admin' ? 'disabled' : ''}>
<span class="toggle-slider"></span>
<span class="visually-hidden">Stato attivo utente ${escapeHtml(user.email || '')}</span>
<input type="checkbox" ${isActive ? 'checked' : ''} onchange="toggleUserActive(${user.id}, this.checked)" ${role === 'super_admin' ? 'disabled' : ''} aria-label="${isActive ? 'Disattiva' : 'Attiva'} utente ${escapeHtml(user.email || '')}">
<span class="toggle-slider" aria-hidden="true"></span>
</label>
</td>
<td>${formatDate(user.created_at)}</td>
<td>
${role !== 'super_admin' && isActive
? `<button class="btn btn-secondary btn-sm" onclick="impersonateUser(${user.id}, '${escapeHtml(user.email)}')" title="Entra come questo utente per 1 ora">
<svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor" style="vertical-align:-2px"><path d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"/></svg>
<svg class="icon icon-sm" aria-hidden="true" style="vertical-align:-2px"><use href="../vendor/bootstrap-italia/dist/svg/sprites.svg#it-exchange-circle"></use></svg>
Impersonate
</button>`
: '<span class="text-muted">-</span>'}
@@ -355,7 +374,7 @@
sessionStorage.setItem('nis2_impersonate_target_email', email);
// Sostituisce token con quello impersonate (no refresh per default)
api.setTokens(res.data.access_token, '');
window.location.href = '/dashboard.html';
window.location.href = '../dashboard.html';
} else {
showNotification(res.message || 'Impersonate fallito.', 'error');
}
+20 -6
View File
@@ -4,7 +4,11 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gap Analysis - NIS2 Agile</title>
<link rel="stylesheet" href="css/style.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 (wizard/form/gauge) -> 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">
</head>
<body>
<div class="app-layout">
@@ -14,7 +18,7 @@
<!-- Main Content -->
<main class="main-content">
<header class="content-header">
<h2 data-i18n="assessment.title">Gap Analysis NIS2</h2>
<h1 class="h2" data-i18n="assessment.title">Gap Analysis NIS2</h1>
<div class="content-header-actions">
<button class="btn btn-outline btn-sm" id="btn-ai-analyze" style="display:none;" onclick="aiAnalyze()">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zm7-10a1 1 0 01.967.744L14.146 7.2 17.5 8.512a1 1 0 010 1.836l-3.354 1.311-1.18 4.456a1 1 0 01-1.932 0L9.854 11.66 6.5 10.348a1 1 0 010-1.836l3.354-1.311 1.18-4.456A1 1 0 0112 2z"/></svg>
@@ -137,10 +141,20 @@
</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 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 src="/js/help.js"></script>
<script>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+35 -21
View File
@@ -4,6 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inventario Asset - NIS2 Agile</title>
<!-- 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 (tabelle/modali/form) -> 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>
/* ── Stats Cards ────────────────────────────────────────── */
@@ -261,7 +265,7 @@
<main class="main-content">
<header class="content-header">
<h2 data-i18n="assets.title">Inventario Asset</h2>
<h1 class="h2" data-i18n="assets.title">Inventario Asset</h1>
<div class="content-header-actions">
<span class="text-muted" style="font-size:0.8rem; margin-right:8px;">Art. 21.2.i NIS2</span>
<button class="btn btn-secondary" onclick="openImportModal()" title="Importa asset da CSV/CMDB">Importa</button>
@@ -313,7 +317,7 @@
<!-- Filters -->
<div class="filters-bar">
<select id="filter-type" onchange="applyFilters()">
<select id="filter-type" onchange="applyFilters()" aria-label="Filtra per tipo">
<option value="">Tutti i tipi</option>
<option value="hardware">Hardware</option>
<option value="software">Software</option>
@@ -323,14 +327,14 @@
<option value="personnel">Personale</option>
<option value="facility">Struttura</option>
</select>
<select id="filter-criticality" onchange="applyFilters()">
<select id="filter-criticality" onchange="applyFilters()" aria-label="Filtra per criticità">
<option value="">Tutte le criticita'</option>
<option value="critical">Critica</option>
<option value="high">Alta</option>
<option value="medium">Media</option>
<option value="low">Bassa</option>
</select>
<select id="filter-status" onchange="applyFilters()">
<select id="filter-status" onchange="applyFilters()" aria-label="Filtra per stato">
<option value="">Tutti gli stati</option>
<option value="active">Attivo</option>
<option value="maintenance">Manutenzione</option>
@@ -351,6 +355,16 @@
<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 src="/js/help.js"></script>
<script>
@@ -549,7 +563,7 @@
body += `<div class="form-group" style="margin-bottom:0.75rem;">
<label class="form-label" style="font-weight:600;">${def.label} <span style="color:var(--gray-400);font-weight:400;">(max ${def.max})</span></label>
<div style="font-size:0.78rem;color:var(--gray-500);margin-bottom:4px;">${def.help}</div>
<select class="form-select score-criterion" data-key="${key}" onchange="updateScorePreview()">${opts}</select>
<select class="form-select score-criterion" data-key="${key}" onchange="updateScorePreview()" aria-label="${def.label}">${opts}</select>
</div>`;
}
body += `<div id="score-preview" style="margin-top:1rem;padding:0.9rem;border-radius:10px;background:var(--gray-50);font-weight:600;text-align:center;">
@@ -763,70 +777,70 @@
showModal(title, `
<div class="form-group">
<label class="form-label">Nome *</label>
<label for="asset-name" class="form-label">Nome *</label>
<input type="text" class="form-input" id="asset-name" value="${escapeHtml(a.name || '')}" required>
</div>
<div class="form-grid-2">
<div class="form-group">
<label class="form-label">Tipo *</label>
<label for="asset-type" class="form-label">Tipo *</label>
<select class="form-select" id="asset-type">${typeOptions}</select>
</div>
<div class="form-group">
<label class="form-label">Categoria</label>
<label for="asset-category" class="form-label">Categoria</label>
<input type="text" class="form-input" id="asset-category" value="${escapeHtml(a.category || '')}" placeholder="Es: Server, Database...">
</div>
</div>
<div class="form-group">
<label class="form-label">Descrizione</label>
<label for="asset-description" class="form-label">Descrizione</label>
<textarea class="form-input" id="asset-description" rows="2">${escapeHtml(a.description || '')}</textarea>
</div>
<div class="form-grid-2">
<div class="form-group">
<label class="form-label">Criticita'</label>
<label for="asset-criticality" class="form-label">Criticita'</label>
<select class="form-select" id="asset-criticality">${critOptions}</select>
</div>
<div class="form-group">
<label class="form-label">Stato</label>
<label for="asset-status" class="form-label">Stato</label>
<select class="form-select" id="asset-status">${statusOptions}</select>
</div>
</div>
<div class="form-grid-2">
<div class="form-group">
<label class="form-label">Owner (ID utente)</label>
<label for="asset-owner" class="form-label">Owner (ID utente)</label>
<input type="text" class="form-input" id="asset-owner" value="${escapeHtml(a.owner_user_id || '')}">
</div>
<div class="form-group">
<label class="form-label">Posizione</label>
<label for="asset-location" class="form-label">Posizione</label>
<input type="text" class="form-input" id="asset-location" value="${escapeHtml(a.location || '')}" placeholder="Es: Data Center Roma">
</div>
</div>
<div class="form-grid-2">
<div class="form-group">
<label class="form-label">Indirizzo IP</label>
<label for="asset-ip" class="form-label">Indirizzo IP</label>
<input type="text" class="form-input" id="asset-ip" value="${escapeHtml(a.ip_address || '')}" placeholder="Es: 192.168.1.100">
</div>
<div class="form-group">
<label class="form-label">Fornitore</label>
<label for="asset-vendor" class="form-label">Fornitore</label>
<input type="text" class="form-input" id="asset-vendor" value="${escapeHtml(a.vendor || '')}">
</div>
</div>
<div class="form-grid-2">
<div class="form-group">
<label class="form-label">Versione</label>
<label for="asset-version" class="form-label">Versione</label>
<input type="text" class="form-input" id="asset-version" value="${escapeHtml(a.version || '')}">
</div>
<div class="form-group">
<label class="form-label">Numero Seriale</label>
<label for="asset-serial" class="form-label">Numero Seriale</label>
<input type="text" class="form-input" id="asset-serial" value="${escapeHtml(a.serial_number || '')}">
</div>
</div>
<div class="form-grid-2">
<div class="form-group">
<label class="form-label">Data Acquisto</label>
<label for="asset-purchase-date" class="form-label">Data Acquisto</label>
<input type="date" class="form-input" id="asset-purchase-date" value="${a.purchase_date || ''}">
</div>
<div class="form-group">
<label class="form-label">Scadenza Garanzia</label>
<label for="asset-warranty" class="form-label">Scadenza Garanzia</label>
<input type="date" class="form-input" id="asset-warranty" value="${a.warranty_expiry || ''}">
</div>
</div>
@@ -877,8 +891,8 @@ function openImportModal(){
<p style="font-size:.85rem;color:var(--gray-600);margin-bottom:10px;">Incolla CSV o carica un file. Colonne riconosciute:
<code>name, asset_type, criticality, data_classification, internet_facing, dependencies_count, regulated, external_ref, vendor, location</code>.
Lo scoring di rilevanza NIS2 (GV.OC-04) viene calcolato automaticamente.</p>
<div class="form-group"><input type="file" accept=".csv,text/csv" id="imp-file" onchange="impFile(event)"></div>
<div class="form-group"><label class="form-label">Oppure incolla CSV (prima riga = intestazioni)</label>
<div class="form-group"><input type="file" accept=".csv,text/csv" id="imp-file" onchange="impFile(event)" aria-label="Seleziona file CSV da importare"></div>
<div class="form-group"><label for="imp-csv" class="form-label">Oppure incolla CSV (prima riga = intestazioni)</label>
<textarea class="form-input" id="imp-csv" rows="8" placeholder="name,asset_type,criticality,data_classification,internet_facing,dependencies_count,regulated
Core ERP,software,critical,financial,false,8,true
Web Portal,service,high,personal,true,3,true"></textarea></div>
+6
View File
@@ -4,6 +4,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Le Mie Aziende - NIS2 Agile</title>
<!-- Bootstrap Italia v2.18.1 self-hostato (MAI CDN). Caricato PRIMA di style.css: BI veste la chrome/sidebar, style.css resta autorevole per i widget. -->
<link rel="stylesheet" href="vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<link rel="stylesheet" href="css/style.css">
<style>
.companies-header {
@@ -336,6 +338,10 @@
<script src="js/api.js"></script>
<script src="js/common.js"></script>
<!-- Bootstrap Italia bundle + override sidebar BI (common-bi.js dopo common.js: ridefinisce solo loadSidebar) -->
<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 src="js/help.js"></script>
<script>
+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>
+20 -6
View File
@@ -4,7 +4,11 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - NIS2 Agile</title>
<link rel="stylesheet" href="css/style.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 (card/gauge/stat/modali) -> 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">
</head>
<body>
<div class="app-layout">
@@ -14,7 +18,7 @@
<!-- Main Content -->
<main class="main-content">
<header class="content-header">
<h2 data-i18n="dashboard.title">Dashboard</h2>
<h1 class="h2" data-i18n="dashboard.title">Dashboard</h1>
<div class="content-header-actions">
<span class="text-muted" id="header-date"></span>
</div>
@@ -126,10 +130,20 @@
</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 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 src="/js/help.js"></script>
<script>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+46 -40
View File
@@ -4,56 +4,62 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Password dimenticata - 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 da /vendor (MAI CDN). Variante "-bi" della forgot-password.html. -->
<link rel="stylesheet" href="vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<style>
.back-link { display:block; text-align:center; margin-top:14px; font-size:.85rem; color:#6B7280; text-decoration:none; }
.back-link:hover { color: var(--color-primary, #2563eb); }
.auth-success { background:#ECFDF5; color:#065F46; border:1px solid #A7F3D0; padding:12px 16px; border-radius:6px; font-size:.9rem; margin-bottom:16px; display:none; }
.auth-success.visible { display:block; }
.auth-helper { font-size:.85rem; color:#6B7280; margin-bottom:18px; line-height:1.5; }
body { min-height: 100vh; display: flex; align-items: center; background: #f5f6f7; }
.auth-wrap { width: 100%; max-width: 460px; margin: 0 auto; padding: 24px; }
.auth-brand { text-align: center; margin-bottom: 24px; }
.auth-brand .brand-name { font-weight: 700; font-size: 1.5rem; color: #06c; }
.auth-brand .brand-name span { color: #17324d; }
.auth-brand .brand-sub { color: #5a6772; font-size: .9rem; margin-top: 4px; }
.ico { width: 1em; height: 1em; fill: currentColor; vertical-align: -.125em; }
</style>
</head>
<body>
<div class="auth-page">
<div class="auth-card">
<div class="auth-header">
<div class="auth-logo">
<div class="auth-logo-icon">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 2.18l7 3.12v4.7c0 4.83-3.23 9.36-7 10.57-3.77-1.21-7-5.74-7-10.57V6.3l7-3.12z"/>
</svg>
</div>
<span class="auth-logo-text">NIS2 <span>Agile</span></span>
</div>
<p class="auth-subtitle">Reimposta la tua password</p>
</div>
<main class="auth-wrap">
<div class="auth-brand">
<div class="brand-name">NIS2 <span>Agile</span></div>
<div class="brand-sub">Reimposta la tua password</div>
</div>
<div class="auth-body">
<div class="auth-error" id="err"></div>
<div class="auth-success" id="ok"></div>
<div class="card card-bg has-bkg-grey shadow">
<div class="card-body">
<h1 class="h4 mb-3">Password dimenticata</h1>
<p class="auth-helper">Inserisci l'indirizzo email associato al tuo account. Ti invieremo un link valido 30 minuti per impostare una nuova password.</p>
<div class="alert alert-danger d-none" id="err" role="alert"></div>
<div class="alert alert-success d-none" id="ok" role="status"></div>
<p class="text-muted small mb-4">Inserisci l'indirizzo email associato al tuo account. Ti invieremo un link valido 30 minuti per impostare una nuova password.</p>
<form id="forgot-form" novalidate>
<div class="form-group">
<label class="form-label" for="email">Indirizzo Email</label>
<input type="email" id="email" name="email" class="form-input"
placeholder="nome@azienda.it" autocomplete="email" required>
<label for="email" class="active">Indirizzo Email</label>
<input type="email" id="email" name="email" class="form-control"
autocomplete="email" required aria-describedby="err">
</div>
<button type="submit" class="btn btn-primary btn-lg w-full" id="submit-btn">
<button type="submit" class="btn btn-primary w-100 mt-2" id="submit-btn">
Invia link
</button>
</form>
</div>
<div class="auth-footer">
<a href="login.html" class="back-link"><i class="fas fa-arrow-left"></i> Torna al login</a>
</div>
</div>
</div>
<p class="text-center mt-3 mb-0">
<a href="login.html">
<svg class="ico me-1" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-arrow-left"></use></svg>
Torna al login
</a>
</p>
</main>
<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>
<!-- Stessa logica della forgot-password.html: ZERO modifiche backend -->
<script src="js/api.js"></script>
<script>
const form = document.getElementById('forgot-form');
@@ -63,13 +69,13 @@
form.addEventListener('submit', async function(e) {
e.preventDefault();
err.classList.remove('visible');
ok.classList.remove('visible');
err.classList.add('d-none');
ok.classList.add('d-none');
const email = document.getElementById('email').value.trim();
if (!email) {
err.textContent = 'Inserisci l\'indirizzo email.';
err.classList.add('visible');
err.classList.remove('d-none');
return;
}
@@ -84,18 +90,18 @@
const data = await res.json();
if (res.status === 429) {
err.textContent = data.message || 'Troppe richieste. Riprova più tardi.';
err.classList.add('visible');
err.classList.remove('d-none');
} else if (data.success) {
ok.textContent = data.message;
ok.classList.add('visible');
ok.classList.remove('d-none');
form.style.display = 'none';
} else {
err.textContent = data.message || 'Errore. Riprova.';
err.classList.add('visible');
err.classList.remove('d-none');
}
} catch (e) {
err.textContent = 'Errore di connessione al server.';
err.classList.add('visible');
err.classList.remove('d-none');
} finally {
btn.disabled = false;
btn.textContent = 'Invia link';
+1075 -1025
View File
File diff suppressed because it is too large Load Diff
+45 -31
View File
@@ -4,6 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gestione Incidenti - NIS2 Agile</title>
<!-- 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 (tabelle/decision-tree/modali) -> 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>
/* ── Filters Bar ─────────────────────────────────────── */
@@ -262,7 +266,7 @@
<main class="main-content">
<header class="content-header">
<h2 data-i18n="incidents.title">Gestione Incidenti</h2>
<h1 class="h2" data-i18n="incidents.title">Gestione Incidenti</h1>
<div class="content-header-actions">
<button class="btn btn-primary" onclick="showCreateIncidentModal()">+ Nuovo Incidente</button>
</div>
@@ -272,7 +276,7 @@
<!-- List View -->
<div id="view-list">
<div class="filters-bar">
<select class="form-select" id="filter-classification" onchange="loadIncidents()">
<select class="form-select" id="filter-classification" onchange="loadIncidents()" aria-label="Filtra per classificazione">
<option value="">Tutte le classificazioni</option>
<option value="cyber_attack">Attacco Cyber</option>
<option value="data_breach">Data Breach</option>
@@ -282,14 +286,14 @@
<option value="supply_chain">Supply Chain</option>
<option value="other">Altro</option>
</select>
<select class="form-select" id="filter-severity" onchange="loadIncidents()">
<select class="form-select" id="filter-severity" onchange="loadIncidents()" aria-label="Filtra per gravità">
<option value="">Tutte le severita'</option>
<option value="critical">Critico</option>
<option value="high">Alto</option>
<option value="medium">Medio</option>
<option value="low">Basso</option>
</select>
<select class="form-select" id="filter-status" onchange="loadIncidents()">
<select class="form-select" id="filter-status" onchange="loadIncidents()" aria-label="Filtra per stato">
<option value="">Tutti gli stati</option>
<option value="detected">Rilevato</option>
<option value="analyzing">In Analisi</option>
@@ -336,6 +340,16 @@
<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 src="/js/help.js"></script>
<script>
@@ -467,7 +481,7 @@
<div class="nis2-compact">
<span class="nis2-dot ${ew}" title="Early Warning 24h"></span>
<span class="nis2-dot ${nt}" title="Notifica 72h"></span>
<span class="nis2-dot ${fr}" title="Report Finale 30gg"></span>
<span class="nis2-dot ${fr}" title="Report Finale 1 mese"></span>
${ew === 'overdue' || nt === 'overdue' || fr === 'overdue'
? '<span class="overdue-icon"><svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg></span>'
: ''}
@@ -485,16 +499,16 @@
const content = `
<div class="form-group">
<label class="form-label">Titolo <span class="required">*</span></label>
<label for="inc-title" class="form-label">Titolo <span class="required">*</span></label>
<input type="text" class="form-input" id="inc-title" placeholder="Titolo dell'incidente">
</div>
<div class="form-group">
<label class="form-label">Descrizione <span class="required">*</span></label>
<label for="inc-description" class="form-label">Descrizione <span class="required">*</span></label>
<textarea class="form-textarea" id="inc-description" rows="3" placeholder="Descrizione dettagliata dell'incidente"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Classificazione</label>
<label for="inc-classification" class="form-label">Classificazione</label>
<select class="form-select" id="inc-classification">
<option value="cyber_attack">Attacco Cyber</option>
<option value="data_breach">Data Breach</option>
@@ -506,7 +520,7 @@
</select>
</div>
<div class="form-group">
<label class="form-label">Severita'</label>
<label for="inc-severity" class="form-label">Severita'</label>
<select class="form-select" id="inc-severity">
<option value="low">Basso</option>
<option value="medium" selected>Medio</option>
@@ -516,7 +530,7 @@
</div>
</div>
<div class="form-group">
<label class="form-label">Data e ora rilevamento <span class="required">*</span></label>
<label for="inc-detected-at" class="form-label">Data e ora rilevamento <span class="required">*</span></label>
<input type="datetime-local" class="form-input" id="inc-detected-at" value="${dtLocal}">
</div>
<!-- Decision tree Art.23 NIS2 - significativita' incidente -->
@@ -527,23 +541,23 @@
</div>
<p style="font-size:0.75rem; color:var(--gray-500); margin-bottom:10px;">Un incidente e' significativo se soddisfa almeno uno dei criteri seguenti (Considerando 101 NIS2 / D.Lgs. 138/2024 art. 25):</p>
<div style="display:grid; gap:6px;">
<label style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
<label for="crit-users" style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
<input type="checkbox" id="crit-users" onchange="evalSignificance()" style="accent-color:var(--primary); width:16px; height:16px;">
<span>Ha causato o puo' causare interruzione grave dei servizi per <strong>&ge;500 utenti</strong></span>
</label>
<label style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
<label for="crit-duration" style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
<input type="checkbox" id="crit-duration" onchange="evalSignificance()" style="accent-color:var(--primary); width:16px; height:16px;">
<span>Durata dell'interruzione superiore a <strong>4 ore</strong></span>
</label>
<label style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
<label for="crit-crossborder" style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
<input type="checkbox" id="crit-crossborder" onchange="evalSignificance()" style="accent-color:var(--primary); width:16px; height:16px;">
<span>Impatto su altri Stati Membri UE (impatto transfrontaliero)</span>
</label>
<label style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
<label for="crit-attack" style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
<input type="checkbox" id="crit-attack" onchange="evalSignificance()" style="accent-color:var(--primary); width:16px; height:16px;">
<span>E' un attacco cyber o data breach con dati personali coinvolti</span>
</label>
<label style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
<label for="crit-financial" style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
<input type="checkbox" id="crit-financial" onchange="evalSignificance()" style="accent-color:var(--primary); width:16px; height:16px;">
<span>Perdita finanziaria stimata superiore a <strong>€100.000</strong></span>
</label>
@@ -551,29 +565,29 @@
<div id="significance-result" style="margin-top:10px; padding:8px 12px; border-radius:6px; font-size:0.8rem; font-weight:600; display:none;"></div>
</div>
<div class="form-group">
<label class="form-check-inline">
<label for="inc-significant" class="form-check-inline">
<input type="checkbox" id="inc-significant">
<span><strong>Incidente significativo</strong> (richiede notifica CSIRT/ACN ai sensi dell'Art. 23 NIS2)</span>
</label>
</div>
<div class="form-group">
<label class="form-label">Servizi interessati</label>
<label for="inc-affected-services" class="form-label">Servizi interessati</label>
<textarea class="form-textarea" id="inc-affected-services" rows="2" placeholder="Elenco dei servizi e sistemi interessati"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Utenti coinvolti</label>
<label for="inc-affected-users" class="form-label">Utenti coinvolti</label>
<input type="number" class="form-input" id="inc-affected-users" min="0" placeholder="0">
</div>
</div>
<div class="form-group">
<label class="form-check-inline">
<label for="inc-cross-border" class="form-check-inline">
<input type="checkbox" id="inc-cross-border">
<span>Impatto transfrontaliero</span>
</label>
</div>
<div class="form-group">
<label class="form-check-inline">
<label for="inc-malicious" class="form-check-inline">
<input type="checkbox" id="inc-malicious">
<span>Azione dolosa / malevola</span>
</label>
@@ -638,7 +652,7 @@
const content = `
<div class="form-group">
<label class="form-label">Stato</label>
<label for="upd-status" class="form-label">Stato</label>
<select class="form-select" id="upd-status">
${Object.entries(STATUS_LABELS).map(([k, v]) =>
`<option value="${k}" ${incident.status === k ? 'selected' : ''}>${v}</option>`
@@ -646,15 +660,15 @@
</select>
</div>
<div class="form-group">
<label class="form-label">Causa Radice</label>
<label for="upd-root-cause" class="form-label">Causa Radice</label>
<textarea class="form-textarea" id="upd-root-cause" rows="3" placeholder="Analisi della causa radice">${escapeHtml(incident.root_cause || '')}</textarea>
</div>
<div class="form-group">
<label class="form-label">Azioni di Rimedio</label>
<label for="upd-remediation" class="form-label">Azioni di Rimedio</label>
<textarea class="form-textarea" id="upd-remediation" rows="3" placeholder="Azioni correttive intraprese">${escapeHtml(incident.remediation_actions || '')}</textarea>
</div>
<div class="form-group">
<label class="form-label">Lezioni Apprese</label>
<label for="upd-lessons" class="form-label">Lezioni Apprese</label>
<textarea class="form-textarea" id="upd-lessons" rows="3" placeholder="Lezioni apprese dall'incidente">${escapeHtml(incident.lessons_learned || '')}</textarea>
</div>
`;
@@ -904,7 +918,7 @@
<div class="card-body" style="display:flex; flex-direction:column; gap:8px;">
${!inc.early_warning_sent_at ? `<button class="btn btn-warning w-full" onclick="doSendEarlyWarning(${inc.id})">Invia Early Warning (24h)</button>` : `<button class="btn btn-success w-full" disabled>Early Warning Inviato</button>`}
${!inc.notification_sent_at ? `<button class="btn btn-warning w-full" onclick="doSendNotification(${inc.id})">Invia Notifica (72h)</button>` : `<button class="btn btn-success w-full" disabled>Notifica Inviata</button>`}
${!inc.final_report_sent_at ? `<button class="btn btn-warning w-full" onclick="doSendFinalReport(${inc.id})">Invia Report Finale (30gg)</button>` : `<button class="btn btn-success w-full" disabled>Report Finale Inviato</button>`}
${!inc.final_report_sent_at ? `<button class="btn btn-warning w-full" onclick="doSendFinalReport(${inc.id})">Invia Report Finale (1 mese)</button>` : `<button class="btn btn-success w-full" disabled>Report Finale Inviato</button>`}
</div>
</div>` : ''}
</div>
@@ -932,10 +946,10 @@
actionLabel: 'Invia Notifica'
},
{
title: 'Report Finale (30 giorni)',
title: 'Report Finale (1 mese)',
dueDate: inc.final_report_due,
sentAt: inc.final_report_sent_at,
description: 'Report finale completo con analisi della causa radice, impatto e lezioni apprese entro 30 giorni.',
description: 'Report finale completo con analisi della causa radice, impatto e lezioni apprese entro 1 mese.',
action: `doSendFinalReport(${inc.id})`,
actionLabel: 'Invia Report Finale'
}
@@ -1092,7 +1106,7 @@
async function doSendFinalReport(id) {
showModal('Conferma Report Finale', `
<p>Stai per registrare l'invio del <strong>Report Finale (30 giorni)</strong> al CSIRT nazionale (ACN).</p>
<p>Stai per registrare l'invio del <strong>Report Finale (1 mese)</strong> al CSIRT nazionale (ACN).</p>
<p class="text-muted" style="margin-top:8px; font-size:0.85rem;">
Il report finale deve includere la descrizione dettagliata dell'incidente, la causa radice,
le misure di mitigazione applicate e l'impatto transfrontaliero, se applicabile.
@@ -1125,7 +1139,7 @@
function showAddEventModal(incidentId) {
const content = `
<div class="form-group">
<label class="form-label">Tipo Evento</label>
<label for="event-type" class="form-label">Tipo Evento</label>
<select class="form-select" id="event-type">
<option value="detection">Rilevamento</option>
<option value="analysis">Analisi</option>
@@ -1139,7 +1153,7 @@
</select>
</div>
<div class="form-group">
<label class="form-label">Descrizione <span class="required">*</span></label>
<label for="event-description" class="form-label">Descrizione <span class="required">*</span></label>
<textarea class="form-textarea" id="event-description" rows="3" placeholder="Descrizione dell'evento"></textarea>
</div>
`;
@@ -1198,7 +1212,7 @@
resultDiv.style.background = '#fef2f2';
resultDiv.style.border = '1px solid #fca5a5';
resultDiv.style.color = 'var(--danger)';
resultDiv.innerHTML = '&#9888; ' + checkedCount + ' criteri soddisfatti — l\'incidente e\' probabilmente <strong>SIGNIFICATIVO</strong>. Attivare le notifiche Art. 23 (24h/72h/30gg).';
resultDiv.innerHTML = '&#9888; ' + checkedCount + ' criteri soddisfatti — l\'incidente e\' probabilmente <strong>SIGNIFICATIVO</strong>. Attivare le notifiche Art. 23 (24h/72h/1 mese).';
if (sigCheckbox) sigCheckbox.checked = true;
} else {
resultDiv.style.background = '#f0fdf4';
+41 -21
View File
@@ -4,7 +4,11 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Modello Organizzativo SGSI (ISO 27001) - NIS2 Agile</title>
<link rel="stylesheet" href="css/style.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 (wizard/SoA/tabelle) -> 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>
.isms-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:20px; font-size:.92rem; line-height:1.6; }
.isms-steps { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:20px; }
@@ -40,6 +44,8 @@
.ck-item { display:flex; justify-content:space-between; padding:8px 0; border-bottom:1px solid var(--gray-100,#f3f4f6); font-size:.9rem; }
.ck-yes { color:#16a34a; font-weight:700; } .ck-no { color:#9ca3af; }
.savehint { font-size:.78rem; color:var(--gray-400,#9ca3af); min-height:16px; }
/* Allinea l'icona sprite BI nel bottone Esporta col testo */
.content-header-actions .btn .icon { vertical-align:text-bottom; margin-right:4px; }
</style>
</head>
<body>
@@ -49,8 +55,11 @@
<header class="content-header">
<h2 data-i18n="isms.title">Modello Organizzativo SGSI (ISO 27001)</h2>
<div class="content-header-actions">
<span class="savehint" id="isms-savehint"></span>
<button class="btn btn-outline btn-sm" onclick="ismsExportView()" data-i18n="isms.export">Esporta SGSI</button>
<span class="savehint" id="isms-savehint" role="status" aria-live="polite"></span>
<button class="btn btn-outline btn-sm" onclick="ismsExportView()" data-i18n="isms.export">
<svg class="icon icon-sm" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-file"></use></svg>
<span>Esporta SGSI</span>
</button>
</div>
</header>
@@ -67,12 +76,12 @@
<div class="isms-panel" id="panel-0">
<div class="card"><div class="card-body">
<h3 data-i18n="isms.s1.title">1. Contesto e Ambito (cl. 4)</h3>
<div class="isms-field"><label data-i18n="isms.s1.scope">Ambito del SGSI (scope statement)</label><textarea id="f-scope" rows="3" placeholder="Es. Tutti i sistemi e processi a supporto dei servizi ICT erogati dalla sede di..."></textarea></div>
<div class="isms-field"><label data-i18n="isms.s1.ctx_int">Contesto interno</label><textarea id="f-ctx-int" rows="2"></textarea></div>
<div class="isms-field"><label data-i18n="isms.s1.ctx_ext">Contesto esterno</label><textarea id="f-ctx-ext" rows="2"></textarea></div>
<div class="isms-field"><label data-i18n="isms.s1.parties">Parti interessate (una per riga)</label><textarea id="f-parties" rows="3" placeholder="Clienti&#10;Autorità (ACN/CSIRT)&#10;Fornitori ICT"></textarea></div>
<div class="isms-field"><label data-i18n="isms.s1.boundaries">Confini del SGSI</label><textarea id="f-boundaries" rows="2"></textarea></div>
<div class="isms-field"><label data-i18n="isms.s1.exclusions">Esclusioni motivate</label><textarea id="f-exclusions" rows="2"></textarea></div>
<div class="isms-field"><label for="f-scope" data-i18n="isms.s1.scope">Ambito del SGSI (scope statement)</label><textarea id="f-scope" rows="3" placeholder="Es. Tutti i sistemi e processi a supporto dei servizi ICT erogati dalla sede di..."></textarea></div>
<div class="isms-field"><label for="f-ctx-int" data-i18n="isms.s1.ctx_int">Contesto interno</label><textarea id="f-ctx-int" rows="2"></textarea></div>
<div class="isms-field"><label for="f-ctx-ext" data-i18n="isms.s1.ctx_ext">Contesto esterno</label><textarea id="f-ctx-ext" rows="2"></textarea></div>
<div class="isms-field"><label for="f-parties" data-i18n="isms.s1.parties">Parti interessate (una per riga)</label><textarea id="f-parties" rows="3" placeholder="Clienti&#10;Autorità (ACN/CSIRT)&#10;Fornitori ICT"></textarea></div>
<div class="isms-field"><label for="f-boundaries" data-i18n="isms.s1.boundaries">Confini del SGSI</label><textarea id="f-boundaries" rows="2"></textarea></div>
<div class="isms-field"><label for="f-exclusions" data-i18n="isms.s1.exclusions">Esclusioni motivate</label><textarea id="f-exclusions" rows="2"></textarea></div>
<div class="isms-flags">
<strong data-i18n="isms.s1.cloud">Estensioni cloud (attivano i controlli condizionali nel SoA)</strong>
<label><input type="checkbox" id="f-uses-cloud"> <span data-i18n="isms.s1.uses_cloud">Usiamo servizi cloud pubblici (Microsoft 365, AWS, Google...) → ISO 27017</span></label>
@@ -90,9 +99,9 @@
<p class="text-muted" data-i18n="isms.s2.hint">Definisci i ruoli del SGSI con la matrice RACI (Responsible, Accountable, Consulted, Informed).</p>
<div id="roles-list"></div>
<div class="raci-row" style="border-top:2px solid var(--gray-200,#e5e7eb); margin-top:10px;">
<input id="r-name" placeholder="Ruolo (es. CISO)" style="flex:1 1 160px;">
<input id="r-resp" placeholder="Responsabilità" style="flex:2 1 220px;">
<select id="r-raci"><option value="">RACI</option><option>R</option><option>A</option><option>C</option><option>I</option></select>
<input id="r-name" placeholder="Ruolo (es. CISO)" aria-label="Ruolo" style="flex:1 1 160px;">
<input id="r-resp" placeholder="Responsabilità" aria-label="Responsabilità" style="flex:2 1 220px;">
<select id="r-raci" aria-label="RACI"><option value="">RACI</option><option>R</option><option>A</option><option>C</option><option>I</option></select>
<button class="btn btn-sm btn-primary" onclick="addRole()" data-i18n="isms.s2.add">Aggiungi ruolo</button>
</div>
<button class="btn btn-outline" style="margin-top:14px;" onclick="goStep(2)" data-i18n="isms.next">Continua</button>
@@ -103,8 +112,8 @@
<div class="isms-panel" id="panel-2">
<div class="card"><div class="card-body">
<h3 data-i18n="isms.s3.title">3. Risk: metodologia e obiettivi (cl. 6)</h3>
<div class="isms-field"><label data-i18n="isms.s3.method">Metodologia di risk assessment</label><textarea id="f-method" rows="3" placeholder="Es. ISO 27005 / NIST SP 800-30; scala probabilità×impatto 5×5 (già usata nel modulo Rischi)."></textarea></div>
<div class="isms-field"><label data-i18n="isms.s3.objectives">Obiettivi del SGSI (uno per riga)</label><textarea id="f-objectives" rows="3"></textarea></div>
<div class="isms-field"><label for="f-method" data-i18n="isms.s3.method">Metodologia di risk assessment</label><textarea id="f-method" rows="3" placeholder="Es. ISO 27005 / NIST SP 800-30; scala probabilità×impatto 5×5 (già usata nel modulo Rischi)."></textarea></div>
<div class="isms-field"><label for="f-objectives" data-i18n="isms.s3.objectives">Obiettivi del SGSI (uno per riga)</label><textarea id="f-objectives" rows="3"></textarea></div>
<p class="text-muted" data-i18n="isms.s3.link">Il registro dei rischi vive nel modulo <a href="risks.html">Rischi</a>: questo passo ne dichiara la metodologia.</p>
<button class="btn btn-primary" onclick="saveStep3()" data-i18n="isms.save_next">Salva e continua</button>
</div></div>
@@ -129,7 +138,8 @@
<div class="card"><div class="card-body">
<h3 data-i18n="isms.s5.title">5. Documented Information (cl. 7-8)</h3>
<div class="raci-row" style="border:none;">
<select id="d-type" style="flex:1 1 200px;">
<label for="d-type" class="sr-only" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);">Tipo di documento</label>
<select id="d-type" style="flex:1 1 200px;" aria-label="Tipo di documento">
<option value="policy_sgsi">Policy SGSI</option>
<option value="dichiarazione_applicabilita">Dichiarazione di Applicabilità</option>
<option value="politica_controllo_accessi">Politica controllo accessi</option>
@@ -139,7 +149,7 @@
</select>
<button class="btn btn-sm btn-primary" onclick="aiGenDoc()" id="btn-aigen" data-i18n="isms.s5.aigen">Genera bozza con AI</button>
</div>
<div class="savehint" id="aigen-hint"></div>
<div class="savehint" id="aigen-hint" role="status" aria-live="polite"></div>
<div id="docs-list" style="margin-top:14px;"></div>
<button class="btn btn-outline" style="margin-top:14px;" onclick="goStep(5)" data-i18n="isms.next">Continua</button>
</div></div>
@@ -161,10 +171,20 @@
</main>
</div>
<script src="js/i18n.js"></script>
<script src="js/common.js"></script>
<script src="js/api.js"></script>
<script src="js/help.js"></script>
<script src="js/isms.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 src="/js/help.js"></script>
<script src="/js/isms.js"></script>
</body>
</html>
+15 -13
View File
@@ -15,7 +15,7 @@
* invariati senza toccare api.js.
* - Accessibilità: <nav aria-label>, link active con aria-current,
* bottoni con aria-label, focus nativo BI.
* - I link puntano alle varianti "-bi.html" quando esistono, con
* - I link puntano alle varianti ".html" quando esistono, con
* fallback all'originale per le pagine non ancora convertite.
* ═══════════════════════════════════════════════════════════════════ */
(function () {
@@ -25,17 +25,19 @@
// Per queste la navigazione resta in contesto "-bi". Le altre puntano
// ancora all'originale (parità funzionale, nessun link rotto).
var BI_PAGES = {
'login.html': 'login-bi.html',
'dashboard.html': 'dashboard-bi.html',
'assessment.html': 'assessment-bi.html',
'risks.html': 'risks-bi.html',
'incidents.html': 'incidents-bi.html',
'policies.html': 'policies-bi.html',
'supply-chain.html': 'supply-chain-bi.html',
'training.html': 'training-bi.html',
'assets.html': 'assets-bi.html',
'reports.html': 'reports-bi.html',
'settings.html': 'settings-bi.html'
'login.html': 'login.html',
'dashboard.html': 'dashboard.html',
'assessment.html': 'assessment.html',
'isms.html': 'isms.html',
'risks.html': 'risks.html',
'incidents.html': 'incidents.html',
'policies.html': 'policies.html',
'supply-chain.html': 'supply-chain.html',
'training.html': 'training.html',
'assets.html': 'assets.html',
'reports.html': 'reports.html',
'settings.html': 'settings.html',
'whistleblowing.html': 'whistleblowing.html'
};
function biHref(href) {
@@ -46,7 +48,7 @@
var container = document.getElementById('sidebar');
if (!container) return;
// La pagina corrente può essere sia "x.html" sia "x-bi.html":
// La pagina corrente può essere sia "x.html" sia "x.html":
// normalizziamo togliendo il suffisso -bi per l'evidenziazione active.
var raw = window.location.pathname.split('/').pop() || 'dashboard.html';
var currentBase = raw.replace(/-bi\.html$/, '.html');
+34 -14
View File
@@ -4,7 +4,11 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Knowledge Base - NIS2 Agile</title>
<link rel="stylesheet" href="css/style.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 (lista doc, search) -> 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>
.kb-scope-radios { display: flex; gap: 18px; flex-wrap: wrap; font-size: .9rem; }
.kb-scope-radios label { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
@@ -15,6 +19,8 @@
.kb-scope-badge.SYSTEM { background: #DBEAFE; color: #1E40AF; }
.kb-scope-badge.FIRM { background: #FEF3C7; color: #92400E; }
.kb-scope-badge.ORG { background: #DCFCE7; color: #166534; }
/* Allinea l'icona sprite BI dentro i bottoni del design system esistente */
.btn .icon { fill: currentColor; vertical-align: middle; }
</style>
</head>
<body>
@@ -23,10 +29,10 @@
<main class="main-content">
<header class="content-header">
<h2>Knowledge Base</h2>
<h1 class="h2">Knowledge Base</h1>
<div class="content-header-actions">
<button class="btn btn-primary" id="btn-kb-upload">
<svg viewBox="0 0 20 20" fill="currentColor" style="width:18px;height:18px;vertical-align:middle;"><path d="M5.5 13a3.5 3.5 0 01-.369-6.98 4 4 0 117.753-1.977A4.5 4.5 0 1113.5 13H11V9.413l1.293 1.293a1 1 0 001.414-1.414l-3-3a1 1 0 00-1.414 0l-3 3a1 1 0 001.414 1.414L9 9.414V13H5.5z"/><path d="M9 13h2v5a1 1 0 11-2 0v-5z"/></svg>
<svg class="icon icon-sm" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-upload"></use></svg>
Carica documento
</button>
</div>
@@ -81,7 +87,7 @@
</div>
<div id="kb-share-block" class="kb-share-block" style="display:none;">
<label style="font-size:.78rem;color:var(--gray-600);">
<label for="kb-shared-with" style="font-size:.78rem;color:var(--gray-600);">
Organizzazioni clienti destinatarie (Ctrl/Cmd per selezione multipla, opzionale):
</label>
<select id="kb-shared-with" multiple size="5" class="form-control">
@@ -95,7 +101,7 @@
<div style="margin-top:16px;display:flex;gap:8px;align-items:center;">
<button class="btn btn-primary" id="btn-kb-submit">Indicizza documento</button>
<button class="btn btn-secondary" id="btn-kb-cancel">Annulla</button>
<span id="kb-status" style="margin-left:8px;font-size:.85rem;"></span>
<span id="kb-status" role="status" aria-live="polite" style="margin-left:8px;font-size:.85rem;"></span>
</div>
</div>
</div>
@@ -117,8 +123,12 @@
<div class="card-header"><h3>Test search semantica</h3></div>
<div class="card-body">
<div style="display:flex;gap:8px;">
<input type="text" id="kb-search-query" class="form-control" placeholder="Es: come gestire un incidente NIS2?">
<button class="btn btn-secondary" id="btn-kb-search">Cerca</button>
<label for="kb-search-query" class="sr-only visually-hidden">Query di ricerca semantica</label>
<input type="text" id="kb-search-query" class="form-control" placeholder="Es: come gestire un incidente NIS2?" aria-label="Query di ricerca semantica">
<button class="btn btn-secondary" id="btn-kb-search">
<svg class="icon icon-sm" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-search"></use></svg>
Cerca
</button>
</div>
<div id="kb-search-results" style="margin-top:12px;"></div>
</div>
@@ -127,19 +137,29 @@
</main>
</div>
<script src="js/common.js"></script>
<script src="js/api.js"></script>
<script src="js/i18n.js"></script>
<script src="js/help.js"></script>
<script src="js/kb.js"></script>
<!-- Stessa logica JS della pagina live (parita' funzionale assoluta, zero backend). -->
<script src="/js/common.js"></script>
<script src="/js/api.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. -->
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script>
// Gate auth + chrome come le altre pagine (kb.html ne era priva).
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 src="/js/help.js"></script>
<script src="/js/kb.js"></script>
<script>
// Gate auth + chrome come le altre pagine -bi.
if (typeof checkAuth === 'function' && !checkAuth()) {
// checkAuth reindirizza a login se non autenticato
} else {
if (typeof loadSidebar === 'function') loadSidebar();
if (typeof I18n !== 'undefined' && I18n.init) I18n.init();
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
if (typeof HelpSystem !== 'undefined' && HelpSystem.init) HelpSystem.init();
}
</script>
</body>
+66 -85
View File
@@ -4,136 +4,117 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accedi - 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">
<!-- PILOTA Bootstrap Italia v2.18.1 (Opzione B). Self-hostato: asset serviti da /vendor (regola 'MAI CDN'). -->
<link rel="stylesheet" href="vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<style>
/* solo layout di centratura: i colori/font/forme vengono da Bootstrap Italia */
body { min-height: 100vh; display: flex; align-items: center; background: #f5f6f7; }
.auth-wrap { width: 100%; max-width: 460px; margin: 0 auto; padding: 24px; }
.auth-brand { text-align: center; margin-bottom: 24px; }
.auth-brand .brand-name { font-weight: 700; font-size: 1.5rem; color: #06c; }
.auth-brand .brand-name span { color: #17324d; }
.auth-brand .brand-sub { color: #5a6772; font-size: .9rem; margin-top: 4px; }
.pw-wrap { position: relative; }
.pw-wrap .form-input { padding-right: 42px; }
.pw-toggle {
position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
background: none; border: none; cursor: pointer;
color: #9CA3AF; font-size: 15px; padding: 0; transition: color .2s;
}
.pw-toggle:hover { color: var(--color-primary, #2563eb); }
.forgot-link {
display: block; text-align: center; margin-top: 10px;
font-size: .78rem; color: #6B7280; text-decoration: none; transition: color .2s;
}
.forgot-link:hover { color: var(--color-primary, #2563eb); }
.auth-terms {
margin-top: 14px; padding-top: 14px;
border-top: 1px solid var(--border-color, #e5e7eb);
text-align: center; font-size: .72rem; color: #9CA3AF; line-height: 1.8;
}
.auth-terms a { color: #6B7280; text-decoration: underline; }
.pw-wrap .form-control { padding-right: 44px; }
.pw-toggle { position: absolute; right: 8px; top: 8px; background: none; border: none;
color: #5a6772; cursor: pointer; padding: 6px; }
</style>
</head>
<body>
<div class="auth-page">
<div class="auth-card">
<div class="auth-header">
<div class="auth-logo">
<div class="auth-logo-icon">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 2.18l7 3.12v4.7c0 4.83-3.23 9.36-7 10.57-3.77-1.21-7-5.74-7-10.57V6.3l7-3.12z"/>
<path d="M10 12.5l-2-2-1.41 1.41L10 15.32l5.41-5.41L14 8.5l-4 4z"/>
</svg>
</div>
<span class="auth-logo-text">NIS2 <span>Agile</span></span>
</div>
<p class="auth-subtitle">Piattaforma di compliance NIS2</p>
</div>
<main class="auth-wrap">
<div class="auth-brand">
<div class="brand-name">NIS2 <span>Agile</span></div>
<div class="brand-sub">Piattaforma di compliance NIS2</div>
</div>
<div class="auth-body">
<div class="auth-error" id="login-error"></div>
<div class="card card-bg has-bkg-grey shadow">
<div class="card-body">
<h1 class="h4 mb-3">Accedi</h1>
<div class="alert alert-danger d-none" id="login-error" role="alert"></div>
<form id="login-form" novalidate>
<div class="form-group">
<label class="form-label" for="email">Indirizzo Email</label>
<input type="email" id="email" name="email" class="form-input"
placeholder="nome@azienda.it" autocomplete="email" required>
<label for="email" class="active">Indirizzo Email</label>
<input type="email" id="email" name="email" class="form-control"
autocomplete="email" required aria-describedby="login-error">
</div>
<div class="form-group">
<label class="form-label" for="password">Password</label>
<label for="password" class="active">Password</label>
<div class="pw-wrap">
<input type="password" id="password" name="password" class="form-input"
placeholder="La tua password" autocomplete="current-password" required>
<input type="password" id="password" name="password" class="form-control"
autocomplete="current-password" required>
<button type="button" class="pw-toggle" id="pw-toggle" tabindex="-1"
onclick="(function(){var i=document.getElementById('password'),b=document.getElementById('pw-toggle');if(i.type==='password'){i.type='text';b.innerHTML='<i class=\'fas fa-eye-slash\'></i>';}else{i.type='password';b.innerHTML='<i class=\'fas fa-eye\'></i>';}})()" aria-label="Mostra/nascondi password">
<i class="fas fa-eye"></i>
aria-label="Mostra o nascondi la password"
onclick="(function(){var i=document.getElementById('password');i.type=(i.type==='password')?'text':'password';})()">
<svg class="icon icon-sm" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-password-visible"></use></svg>
</button>
</div>
</div>
<button type="submit" class="btn btn-primary btn-lg w-full" id="login-btn">
Accedi
</button>
<a href="forgot-password.html" class="forgot-link">
Password dimenticata?
</a>
<button type="submit" class="btn btn-primary w-100 mt-2" id="login-btn">Accedi</button>
<p class="text-center mt-3 mb-0">
<a href="forgot-password.html">Password dimenticata?</a>
</p>
</form>
</div>
<div class="auth-footer">
<p>Non hai un account? <a href="register.html">Registrati</a></p>
<div class="auth-terms">
Accedendo accetti i nostri
<a href="https://agentai.agile.software/terms" target="_blank">Termini di Servizio</a>
e la <a href="https://agentai.agile.software/privacy" target="_blank">Privacy Policy</a>
</div>
</div>
</div>
</div>
<p class="text-center mt-3 small">
Non hai un account? <a href="register.html">Registrati</a>
</p>
<p class="text-center small text-muted">
Accedendo accetti i
<a href="https://agentai.agile.software/terms" target="_blank" rel="noopener">Termini di Servizio</a>
e la <a href="https://agentai.agile.software/privacy" target="_blank" rel="noopener">Privacy Policy</a>.
</p>
</main>
<script src="vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script>
// Carica i web font ufficiali (Titillium Web, ecc.) — richiesto da Bootstrap Italia
if (window.bootstrap && bootstrap.loadFonts) {
bootstrap.loadFonts('vendor/bootstrap-italia/dist/fonts');
}
</script>
<!-- Stessa logica della login attuale: ZERO modifiche backend -->
<script src="js/api.js"></script>
<script src="js/common.js"></script>
<script>
// Se gia' autenticato, vai alla dashboard
if (api.isAuthenticated()) {
window.location.href = 'dashboard.html';
}
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
const form = document.getElementById('login-form');
const errorEl = document.getElementById('login-error');
const loginBtn = document.getElementById('login-btn');
function showError(msg) { errorEl.textContent = msg; errorEl.classList.remove('d-none'); }
function hideError() { errorEl.classList.add('d-none'); }
form.addEventListener('submit', async (e) => {
e.preventDefault();
errorEl.classList.remove('visible');
hideError();
const email = document.getElementById('email').value.trim();
const password = document.getElementById('password').value;
if (!email || !password) {
errorEl.textContent = 'Inserisci email e password.';
errorEl.classList.add('visible');
return;
}
if (!email || !password) { showError('Inserisci email e password.'); return; }
loginBtn.disabled = true;
loginBtn.textContent = 'Accesso in corso...';
try {
const result = await api.login(email, password);
if (result.success) {
const isConsultant = result.data.user && result.data.user.role === 'consultant';
const hasOrgs = result.data.organizations && result.data.organizations.length > 0;
if (!hasOrgs) {
window.location.href = 'onboarding.html';
} else if (isConsultant) {
window.location.href = 'companies.html';
} else {
window.location.href = 'dashboard.html';
}
if (!hasOrgs) window.location.href = 'onboarding.html';
else if (isConsultant) window.location.href = 'companies.html';
else window.location.href = 'dashboard.html';
} else {
errorEl.textContent = result.message || 'Credenziali non valide.';
errorEl.classList.add('visible');
showError(result.message || 'Credenziali non valide.');
}
} catch (err) {
errorEl.textContent = 'Errore di connessione al server.';
errorEl.classList.add('visible');
showError('Errore di connessione al server.');
} finally {
loginBtn.disabled = false;
loginBtn.textContent = 'Accedi';
+25 -9
View File
@@ -4,6 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aggiornamenti Normativi - NIS2 Agile</title>
<!-- 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 (card aggiornamenti/badge/modali) -> 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>
.impact-badge { display: inline-flex; align-items: center; padding: 2px 10px; border-radius: 12px; font-size: 0.7rem; font-weight: 700; }
@@ -26,7 +30,7 @@
</head>
<body>
<div class="app-layout">
<div id="sidebar-container"></div>
<aside class="sidebar" id="sidebar"></aside>
<main class="main-content">
<div class="page-header">
<div class="page-header-content">
@@ -35,7 +39,7 @@
</div>
<div class="page-header-actions">
<button class="btn btn-secondary" onclick="loadPending()">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/></svg>
<svg class="icon icon-sm" aria-hidden="true" style="margin-right:6px;"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-clock"></use></svg>
Solo Pendenti
</button>
</div>
@@ -64,7 +68,8 @@
<div class="card mb-16">
<div class="card-body" style="padding:14px 20px;">
<div style="display:flex; gap:12px; flex-wrap:wrap; align-items:center;">
<select id="filter-source" class="form-control" style="width:auto;" onchange="loadUpdates()">
<label for="filter-source" class="visually-hidden">Filtra per fonte normativa</label>
<select id="filter-source" class="form-control" style="width:auto;" onchange="loadUpdates()" aria-label="Filtra per fonte normativa">
<option value="">Tutte le fonti</option>
<option value="dlgs_138_2024">D.Lgs. 138/2024</option>
<option value="nis2_directive">Direttiva NIS2</option>
@@ -73,7 +78,8 @@
<option value="enisa">ENISA</option>
<option value="iso27001">ISO 27001</option>
</select>
<select id="filter-impact" class="form-control" style="width:auto;" onchange="loadUpdates()">
<label for="filter-impact" class="visually-hidden">Filtra per livello di impatto</label>
<select id="filter-impact" class="form-control" style="width:auto;" onchange="loadUpdates()" aria-label="Filtra per livello di impatto">
<option value="">Tutti i livelli</option>
<option value="critical">Critico</option>
<option value="high">Alto</option>
@@ -95,6 +101,16 @@
<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 src="/js/help.js"></script>
<script>
@@ -173,7 +189,7 @@
const container = document.getElementById('updates-container');
if (!updates.length) {
container.innerHTML = `<div class="empty-state">
<svg viewBox="0 0 20 20" fill="currentColor" width="40" height="40" style="color:var(--gray-300)"><path fill-rule="evenodd" d="M6 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V7.414A2 2 0 0015.414 6L12 2.586A2 2 0 0010.586 2H6zm2 10a1 1 0 10-2 0v3a1 1 0 102 0v-3zm2-3a1 1 0 011 1v5a1 1 0 11-2 0v-5a1 1 0 011-1zm4-1a1 1 0 10-2 0v6a1 1 0 102 0V8z" clip-rule="evenodd"/></svg>
<svg class="icon" aria-hidden="true" style="width:40px;height:40px;color:var(--gray-300)"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-file"></use></svg>
<h4>${pendingOnly ? 'Nessun aggiornamento pendente — tutto preso in visione!' : 'Nessun aggiornamento trovato'}</h4>
</div>`;
return;
@@ -196,7 +212,7 @@
<span class="impact-badge impact-${u.impact_level}">${escapeHtml(impactLabels[u.impact_level] || u.impact_level)}</span>
${u.action_required ? '<span style="font-size:0.7rem; font-weight:700; color:var(--warning); background:var(--warning-bg); padding:2px 8px; border-radius:4px;">Azione richiesta</span>' : ''}
${acked
? `<span class="ack-badge ack-done"><svg viewBox="0 0 20 20" fill="currentColor" width="11" height="11"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>Preso in visione ${u.ack_by_name ? 'da ' + escapeHtml(u.ack_by_name) : ''}</span>`
? `<span class="ack-badge ack-done"><svg class="icon" aria-hidden="true" style="width:11px;height:11px;fill:currentColor;"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-check"></use></svg>Preso in visione ${u.ack_by_name ? 'da ' + escapeHtml(u.ack_by_name) : ''}</span>`
: '<span class="ack-badge ack-pending">Da prendere in visione</span>'
}
</div>
@@ -208,11 +224,11 @@
<div style="display:flex; flex-direction:column; gap:8px; align-items:flex-end; flex-shrink:0;">
${u.effective_date ? `<p style="font-size:0.75rem; color:var(--gray-500);">In vigore: <strong>${formatDate(u.effective_date)}</strong></p>` : ''}
${u.url ? `<a href="${escapeHtml(u.url)}" target="_blank" rel="noopener" class="btn btn-sm btn-secondary" style="font-size:0.75rem;">
<svg viewBox="0 0 20 20" fill="currentColor" width="12" height="12"><path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z"/><path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z"/></svg>
<svg class="icon" aria-hidden="true" style="width:12px;height:12px;fill:currentColor;margin-right:4px;"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-external-link"></use></svg>
Fonte
</a>` : ''}
${!acked ? `<button class="btn btn-sm btn-primary" onclick="acknowledgeUpdate(${u.id})">
<svg viewBox="0 0 20 20" fill="currentColor" width="14" height="14"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>
<svg class="icon" aria-hidden="true" style="width:14px;height:14px;fill:currentColor;margin-right:4px;"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-check"></use></svg>
Prendi in visione
</button>` : ''}
</div>
@@ -229,7 +245,7 @@
Documentare la presa visione dell'aggiornamento normativo. Opzionalmente aggiungere note sull'impatto per la tua organizzazione.
</p>
<div class="form-group">
<label class="form-label">Note (opzionale)</label>
<label for="ack-notes" class="form-label">Note (opzionale)</label>
<textarea id="ack-notes" class="form-control" rows="3" placeholder="Es. Analizzato con il team di compliance. Nessun impatto diretto rilevato..."></textarea>
</div>
`, `<button class="btn btn-primary" onclick="confirmAck(${id})">Conferma presa visione</button>
+225 -1104
View File
File diff suppressed because it is too large Load Diff
+24 -10
View File
@@ -4,6 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Policy - NIS2 Agile</title>
<!-- 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 (tabelle/editor/modali) -> 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>
/* ── Policy Page Styles ──────────────────────────────────── */
@@ -242,7 +246,7 @@
<main class="main-content">
<header class="content-header">
<h2 data-i18n="policies.title">Gestione Policy</h2>
<h1 class="h2" data-i18n="policies.title">Gestione Policy</h1>
<div class="content-header-actions">
<button class="btn btn-outline" onclick="openAIGenerateModal()">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zm7-10a1 1 0 01.967.744L14.146 7.2 17.5 8.512a1 1 0 010 1.836l-3.354 1.311-1.18 4.456a1 1 0 01-1.932 0L9.854 11.66 6.5 10.348a1 1 0 010-1.836l3.354-1.311 1.18-4.456A1 1 0 0112 2z"/></svg>
@@ -262,7 +266,7 @@
<!-- Tab: Lista Policy -->
<div class="tab-content active" id="tab-list">
<div class="filters-bar">
<select class="form-select" id="filter-category" onchange="loadPolicies()">
<select class="form-select" id="filter-category" onchange="loadPolicies()" aria-label="Filtra per categoria">
<option value="">Tutte le Categorie</option>
<option value="information_security">Sicurezza Informazioni</option>
<option value="access_control">Controllo Accessi</option>
@@ -275,7 +279,7 @@
<option value="network_security">Sicurezza Rete</option>
<option value="vulnerability_management">Gestione Vulnerabilita'</option>
</select>
<select class="form-select" id="filter-status" onchange="loadPolicies()">
<select class="form-select" id="filter-status" onchange="loadPolicies()" aria-label="Filtra per stato">
<option value="">Tutti gli Stati</option>
<option value="draft">Bozza</option>
<option value="review">In Revisione</option>
@@ -318,6 +322,16 @@
<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 src="/js/help.js"></script>
<script>
@@ -639,34 +653,34 @@
const content = `
<form id="policy-form" onsubmit="return false;">
<div class="form-group">
<label class="form-label">Titolo <span class="required">*</span></label>
<label for="form-title" class="form-label">Titolo <span class="required">*</span></label>
<input type="text" class="form-input" id="form-title" value="${isEdit ? escapeHtml(policy.title) : ''}" required placeholder="Inserisci il titolo della policy">
</div>
<div class="form-row" style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div class="form-group">
<label class="form-label">Categoria <span class="required">*</span></label>
<label for="form-category" class="form-label">Categoria <span class="required">*</span></label>
<select class="form-select" id="form-category" required>
<option value="">Seleziona...</option>
${categoryOptions}
</select>
</div>
<div class="form-group">
<label class="form-label">Articolo NIS2</label>
<label for="form-nis2-article" class="form-label">Articolo NIS2</label>
<input type="text" class="form-input" id="form-nis2-article" value="${isEdit && policy.nis2_article ? escapeHtml(policy.nis2_article) : ''}" placeholder="es. 21.2.a">
</div>
</div>
<div class="form-row" style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div class="form-group">
<label class="form-label">Versione</label>
<label for="form-version" class="form-label">Versione</label>
<input type="text" class="form-input" id="form-version" value="${isEdit && policy.version ? escapeHtml(policy.version) : '1.0'}" placeholder="1.0">
</div>
<div class="form-group">
<label class="form-label">Prossima Revisione</label>
<label for="form-review-date" class="form-label">Prossima Revisione</label>
<input type="date" class="form-input" id="form-review-date" value="${isEdit && policy.next_review_date ? policy.next_review_date.substring(0, 10) : ''}">
</div>
</div>
<div class="form-group">
<label class="form-label">Contenuto</label>
<label for="form-content" class="form-label">Contenuto</label>
<textarea class="form-textarea" id="form-content" rows="12" placeholder="Inserisci il contenuto della policy...&#10;&#10;Puoi strutturare il documento con sezioni:&#10;1. Scopo&#10;2. Ambito&#10;3. Responsabilita'&#10;4. Procedure&#10;5. Revisione">${isEdit && policy.content ? escapeHtml(policy.content) : ''}</textarea>
</div>
</form>
@@ -806,7 +820,7 @@
Il contenuto generato potra' essere rivisto e modificato prima della pubblicazione.
</p>
<div class="form-group">
<label class="form-label">Categoria Policy <span class="required">*</span></label>
<label for="ai-category" class="form-label">Categoria Policy <span class="required">*</span></label>
<select class="form-select" id="ai-category">
${categoryOptions}
</select>
+188 -339
View File
@@ -4,156 +4,74 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Registrazione - 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 da /vendor (MAI CDN). Variante "-bi" della register.html. -->
<link rel="stylesheet" href="vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<style>
/* Step indicator */
.step-indicator {
display: flex; align-items: center; justify-content: center;
gap: 0; margin-bottom: 1.75rem;
}
.step-item {
display: flex; flex-direction: column; align-items: center;
position: relative;
}
.step-circle {
width: 32px; height: 32px; border-radius: 50%;
border: 2px solid var(--border-color, #e5e7eb);
display: flex; align-items: center; justify-content: center;
font-size: .8rem; font-weight: 700;
color: var(--text-secondary, #6b7280);
background: var(--bg-card, #fff);
transition: all .25s;
}
.step-item.active .step-circle {
border-color: var(--color-primary, #2563eb);
color: var(--color-primary, #2563eb);
background: #eff6ff;
}
.step-item.done .step-circle {
border-color: var(--color-success, #10b981);
background: var(--color-success, #10b981);
color: #fff;
}
.step-label {
font-size: .68rem; color: var(--text-secondary, #6b7280);
margin-top: 4px; white-space: nowrap;
}
.step-item.active .step-label { color: var(--color-primary, #2563eb); font-weight: 600; }
.step-connector {
width: 48px; height: 2px;
background: var(--border-color, #e5e7eb);
margin-bottom: 18px; transition: background .25s;
}
.step-connector.done { background: var(--color-success, #10b981); }
body { min-height: 100vh; display: flex; align-items: center; background: #f5f6f7; padding: 24px 0; }
.auth-wrap { width: 100%; max-width: 560px; margin: 0 auto; padding: 24px; }
.auth-brand { text-align: center; margin-bottom: 20px; }
.auth-brand .brand-name { font-weight: 700; font-size: 1.5rem; color: #06c; }
.auth-brand .brand-name span { color: #17324d; }
.auth-brand .brand-sub { color: #5a6772; font-size: .9rem; margin-top: 4px; }
/* Role cards */
.role-grid {
display: grid; grid-template-columns: 1fr 1fr;
gap: .75rem; margin-bottom: 1.25rem;
}
.role-card {
border: 2px solid var(--border-color, #e5e7eb);
border-radius: 12px; padding: 1rem .875rem;
cursor: pointer; transition: all .2s;
background: var(--bg-card, #fff);
text-align: left;
}
.role-card:hover { border-color: #06B6D4; background: #ecfeff; }
.role-card.selected {
border-color: #06B6D4; background: #ecfeff;
box-shadow: 0 0 0 3px rgba(6,182,212,.15);
}
.role-card-icon {
width: 36px; height: 36px; border-radius: 8px;
background: rgba(6,182,212,.12); color: #06B6D4;
display: flex; align-items: center; justify-content: center;
font-size: 1rem; margin-bottom: .6rem;
}
.role-card.selected .role-card-icon { background: rgba(6,182,212,.2); }
.role-card-title { font-weight: 700; font-size: .88rem; color: var(--text-primary, #111); margin-bottom: .2rem; }
.role-card-desc { font-size: .72rem; color: var(--text-secondary, #6b7280); line-height: 1.45; }
/* Step indicator (BI Blu Italia) */
.step-indicator { display: flex; align-items: center; justify-content: center; margin-bottom: 1.5rem; }
.step-item { display: flex; flex-direction: column; align-items: center; }
.step-circle { width: 32px; height: 32px; border-radius: 50%; border: 2px solid #d4d8db;
display: flex; align-items: center; justify-content: center; font-size: .8rem; font-weight: 700;
color: #5a6772; background: #fff; transition: all .25s; }
.step-item.active .step-circle { border-color: #06c; color: #06c; background: #e6f0ff; }
.step-item.done .step-circle { border-color: #008758; background: #008758; color: #fff; }
.step-label { font-size: .68rem; color: #5a6772; margin-top: 4px; white-space: nowrap; }
.step-item.active .step-label { color: #06c; font-weight: 600; }
.step-connector { width: 48px; height: 2px; background: #d4d8db; margin-bottom: 18px; transition: background .25s; }
.step-connector.done { background: #008758; }
/* Invite code section */
.invite-section {
border: 1px dashed var(--border-color, #e5e7eb);
border-radius: 10px; padding: .875rem 1rem; margin-bottom: 1rem;
}
.invite-section summary {
cursor: pointer; font-size: .82rem; font-weight: 600;
color: var(--text-secondary, #6b7280); list-style: none;
display: flex; align-items: center; gap: .5rem;
}
.invite-section summary::-webkit-details-marker { display: none; }
.invite-section[open] summary { color: var(--color-primary, #2563eb); }
.invite-section .form-group { margin-top: .75rem; margin-bottom: 0; }
.invite-badge {
display: inline-flex; align-items: center; gap: .35rem;
font-size: .75rem; padding: .25rem .6rem; border-radius: 20px;
background: #dcfce7; color: #15803d; font-weight: 600;
margin-top: .5rem;
}
/* Role cards (BI palette) */
.role-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .75rem; margin-bottom: 1.25rem; }
.role-card { border: 2px solid #d4d8db; border-radius: 8px; padding: 1rem .875rem; cursor: pointer;
transition: all .2s; background: #fff; text-align: left; }
.role-card:hover { border-color: #06c; background: #e6f0ff; }
.role-card:focus-visible { outline: 2px solid #06c; outline-offset: 2px; }
.role-card.selected { border-color: #06c; background: #e6f0ff; box-shadow: 0 0 0 3px rgba(0,102,204,.15); }
.role-card-icon { width: 36px; height: 36px; border-radius: 8px; background: rgba(0,102,204,.12); color: #06c;
display: flex; align-items: center; justify-content: center; margin-bottom: .6rem; }
.role-card-title { font-weight: 700; font-size: .88rem; color: #17324d; margin-bottom: .2rem; }
.role-card-desc { font-size: .72rem; color: #5a6772; line-height: 1.45; }
.invite-section { border: 1px dashed #d4d8db; border-radius: 8px; padding: .875rem 1rem; margin-bottom: 1rem; }
.invite-section summary { cursor: pointer; font-size: .82rem; font-weight: 600; color: #5a6772; }
.invite-section[open] summary { color: #06c; }
/* Password eye toggle */
.pw-wrap { position: relative; }
.pw-wrap .form-input { padding-right: 42px; }
.pw-toggle {
position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
background: none; border: none; cursor: pointer;
color: #9CA3AF; font-size: 15px; padding: 0; transition: color .2s;
}
.pw-toggle:hover { color: #06B6D4; }
.pw-wrap .form-control { padding-right: 44px; }
.pw-toggle { position: absolute; right: 8px; top: 8px; background: none; border: none; color: #5a6772;
cursor: pointer; padding: 6px; }
/* Lookup status */
.lookup-status {
font-size: .75rem; margin-top: .35rem; min-height: 1.1rem;
display: flex; align-items: center; gap: .35rem;
}
.lookup-status.ok { color: #15803d; }
.lookup-status.err { color: var(--color-danger, #ef4444); }
.lookup-status.loading { color: #6b7280; }
.lookup-status { font-size: .75rem; margin-top: .35rem; min-height: 1.1rem; display: flex; align-items: center; gap: .35rem; }
.lookup-status.ok { color: #008758; }
.lookup-status.err { color: #d9364f; }
.lookup-status.loading { color: #5a6772; }
/* Success step */
.success-body { text-align: center; padding: .5rem 0 1.5rem; }
.success-icon {
width: 64px; height: 64px; border-radius: 50%;
background: linear-gradient(135deg, #06B6D4, #0891b2);
display: flex; align-items: center; justify-content: center;
margin: 0 auto 1.25rem; color: #fff; font-size: 1.75rem;
}
.success-title { font-size: 1.3rem; font-weight: 800; color: var(--text-primary, #111); margin-bottom: .5rem; }
.success-desc { font-size: .9rem; color: var(--text-secondary, #6b7280); line-height: 1.6; margin-bottom: 1.5rem; }
/* Auth terms */
.auth-terms {
margin-top: 14px; padding-top: 14px;
border-top: 1px solid var(--border-color, #e5e7eb);
text-align: center; font-size: .72rem; color: #9CA3AF; line-height: 1.8;
}
.auth-terms a { color: #6B7280; text-decoration: underline; }
.success-body { text-align: center; padding: .5rem 0 1rem; }
.success-icon { width: 64px; height: 64px; border-radius: 50%; background: #008758;
display: flex; align-items: center; justify-content: center; margin: 0 auto 1.25rem; color: #fff; }
/* Steps display */
#step-0, #step-1, #step-2 { display: none; }
#step-0 { display: block; }
.ico { width: 1em; height: 1em; fill: currentColor; vertical-align: -.125em; }
</style>
</head>
<body>
<div class="auth-page">
<div class="auth-card">
<div class="auth-header">
<div class="auth-logo">
<div class="auth-logo-icon">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 2.18l7 3.12v4.7c0 4.83-3.23 9.36-7 10.57-3.77-1.21-7-5.74-7-10.57V6.3l7-3.12z"/>
<path d="M10 12.5l-2-2-1.41 1.41L10 15.32l5.41-5.41L14 8.5l-4 4z"/>
</svg>
</div>
<span class="auth-logo-text">NIS2 <span>Agile</span></span>
</div>
<p class="auth-subtitle" id="auth-subtitle">Crea il tuo account</p>
</div>
<main class="auth-wrap">
<div class="auth-brand">
<h1 class="brand-name">NIS2 <span>Agile</span></h1>
<div class="brand-sub" id="auth-subtitle">Crea il tuo account</div>
</div>
<div class="auth-body">
<div class="card card-bg has-bkg-grey shadow">
<div class="card-body">
<!-- Step indicator -->
<div class="step-indicator" id="step-indicator">
<div class="step-item active" id="si-0">
@@ -172,136 +90,130 @@
</div>
</div>
<div class="auth-error" id="register-error"></div>
<div class="alert alert-danger d-none" id="register-error" role="alert"></div>
<!-- STEP 0: Ruolo + Codice Invito -->
<div id="step-0">
<div class="role-grid">
<div class="role-card" id="card-compliance_manager" onclick="selectRole('compliance_manager')">
<div class="role-card-icon"><i class="fas fa-shield-alt"></i></div>
<div class="role-grid" role="radiogroup" aria-label="Seleziona il tuo ruolo">
<div class="role-card" id="card-compliance_manager" role="radio" aria-checked="false" tabindex="0"
onclick="selectRole('compliance_manager')" onkeydown="cardKey(event,'compliance_manager')">
<div class="role-card-icon"><svg class="ico" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-locked"></use></svg></div>
<div class="role-card-title">CISO / Compliance Manager</div>
<div class="role-card-desc">Responsabile sicurezza e conformità NIS2 dell'organizzazione</div>
</div>
<div class="role-card" id="card-org_admin" onclick="selectRole('org_admin')">
<div class="role-card-icon"><i class="fas fa-building"></i></div>
<div class="role-card" id="card-org_admin" role="radio" aria-checked="false" tabindex="0"
onclick="selectRole('org_admin')" onkeydown="cardKey(event,'org_admin')">
<div class="role-card-icon"><svg class="ico" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-user"></use></svg></div>
<div class="role-card-title">Legale Rappresentante / Admin</div>
<div class="role-card-desc">Amministratore e legale rappresentante dell'organizzazione</div>
</div>
<div class="role-card" id="card-consultant" onclick="selectRole('consultant')">
<div class="role-card-icon"><i class="fas fa-user-tie"></i></div>
<div class="role-card" id="card-consultant" role="radio" aria-checked="false" tabindex="0"
onclick="selectRole('consultant')" onkeydown="cardKey(event,'consultant')">
<div class="role-card-icon"><svg class="ico" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-user"></use></svg></div>
<div class="role-card-title">Consulente Cybersecurity</div>
<div class="role-card-desc">Gestisco la compliance NIS2 per più aziende clienti</div>
</div>
<div class="role-card" id="card-auditor" onclick="selectRole('auditor')">
<div class="role-card-icon"><i class="fas fa-search"></i></div>
<div class="role-card" id="card-auditor" role="radio" aria-checked="false" tabindex="0"
onclick="selectRole('auditor')" onkeydown="cardKey(event,'auditor')">
<div class="role-card-icon"><svg class="ico" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-search"></use></svg></div>
<div class="role-card-title">Auditor / Revisore</div>
<div class="role-card-desc">Verifico e valuto l'implementazione dei controlli di sicurezza</div>
</div>
<div class="role-card" id="card-board_member" onclick="selectRole('board_member')" style="grid-column: span 2;">
<div class="role-card-icon"><i class="fas fa-chess-king"></i></div>
<div class="role-card" id="card-board_member" role="radio" aria-checked="false" tabindex="0"
onclick="selectRole('board_member')" onkeydown="cardKey(event,'board_member')" style="grid-column: span 2;">
<div class="role-card-icon"><svg class="ico" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-key"></use></svg></div>
<div class="role-card-title">Board Member / DPO</div>
<div class="role-card-desc">Membro del consiglio o Responsabile della Protezione dei Dati con visibilità strategica</div>
</div>
</div>
<details class="invite-section" id="invite-details">
<summary><i class="fas fa-ticket-alt"></i> Ho un codice di invito B2B</summary>
<div class="form-group">
<label class="form-label" for="invite-code">Codice Invito</label>
<input type="text" id="invite-code" class="form-input"
<summary>Ho un codice di invito B2B</summary>
<div class="form-group mt-2">
<label for="invite-code">Codice Invito</label>
<input type="text" id="invite-code" class="form-control"
placeholder="es. inv_a1b2c3d4e5f6..." autocomplete="off"
oninput="validateInvite(this.value)">
<div class="lookup-status" id="invite-status"></div>
<div class="lookup-status" id="invite-status" aria-live="polite"></div>
</div>
</details>
<button class="btn btn-primary btn-lg w-full" id="btn-next-0" onclick="goToStep1()" disabled>
Continua <i class="fas fa-arrow-right" style="margin-left:.4rem;"></i>
<button class="btn btn-primary w-100 mt-2" id="btn-next-0" onclick="goToStep1()" disabled>
Continua
<svg class="ico ms-1" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-arrow-right"></use></svg>
</button>
</div>
<!-- STEP 1: Dati account -->
<div id="step-1">
<!-- Banner accesso B2B ridotto (visibile solo con invito + recipient data) -->
<div id="invite-welcome" style="display:none; background:linear-gradient(135deg,#ecfeff,#cffafe); border:1.5px solid #06B6D4; border-radius:12px; padding:.875rem 1rem; margin-bottom:1rem;">
<div style="display:flex; align-items:center; gap:.75rem; margin-bottom:.4rem;">
<div style="width:34px;height:34px;border-radius:8px;background:#06B6D4;color:#fff;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i class="fas fa-ticket-alt" style="font-size:.85rem;"></i>
</div>
<div>
<div id="invite-welcome-name" style="font-weight:700;font-size:.92rem;color:#0e7490;"></div>
<div id="invite-welcome-plan" style="font-size:.73rem;color:#0891b2;margin-top:1px;"></div>
</div>
</div>
<p style="margin:0;font-size:.8rem;color:#0e7490;line-height:1.45;">I tuoi dati sono stati pre-compilati dall'invito. <strong>Scegli solo la password</strong> per accedere alla piattaforma.</p>
<div id="invite-welcome" class="alert alert-info" style="display:none;">
<div id="invite-welcome-name" style="font-weight:700;"></div>
<div id="invite-welcome-plan" style="font-size:.8rem;"></div>
<p class="mb-0 mt-1" style="font-size:.85rem;">I tuoi dati sono stati pre-compilati dall'invito. <strong>Scegli solo la password</strong> per accedere alla piattaforma.</p>
</div>
<form id="register-form" novalidate>
<div style="display:flex; gap:.75rem;">
<div class="form-group" style="flex:1;">
<label class="form-label" for="firstname">Nome <span class="required">*</span></label>
<input type="text" id="firstname" name="firstname" class="form-input"
placeholder="Mario" autocomplete="given-name" required>
<div class="row">
<div class="form-group col">
<label for="firstname" class="active">Nome <span aria-hidden="true">*</span></label>
<input type="text" id="firstname" name="firstname" class="form-control"
autocomplete="given-name" required>
</div>
<div class="form-group" style="flex:1;">
<label class="form-label" for="lastname">Cognome <span class="required">*</span></label>
<input type="text" id="lastname" name="lastname" class="form-input"
placeholder="Rossi" autocomplete="family-name" required>
<div class="form-group col">
<label for="lastname" class="active">Cognome <span aria-hidden="true">*</span></label>
<input type="text" id="lastname" name="lastname" class="form-control"
autocomplete="family-name" required>
</div>
</div>
<div class="form-group">
<label class="form-label" for="email">Indirizzo Email <span class="required">*</span></label>
<input type="email" id="email" name="email" class="form-input"
placeholder="nome@azienda.it" autocomplete="email" required>
<label for="email" class="active">Indirizzo Email <span aria-hidden="true">*</span></label>
<input type="email" id="email" name="email" class="form-control"
autocomplete="email" required>
</div>
<div class="form-group" id="piva-group">
<label class="form-label" for="piva">Partita IVA Azienda <span class="required">*</span></label>
<input type="text" id="piva" name="piva" class="form-input"
placeholder="12345678901" maxlength="11"
<label for="piva" class="active">Partita IVA Azienda <span aria-hidden="true">*</span></label>
<input type="text" id="piva" name="piva" class="form-control"
placeholder="12345678901" maxlength="11" inputmode="numeric"
oninput="lookupPiva(this.value)">
<div class="lookup-status" id="piva-status"></div>
<div class="lookup-status" id="piva-status" aria-live="polite"></div>
</div>
<div class="form-group">
<label class="form-label" for="password">Password <span class="required">*</span></label>
<label for="password" class="active">Password <span aria-hidden="true">*</span></label>
<div class="pw-wrap">
<input type="password" id="password" name="password" class="form-input"
placeholder="Minimo 8 caratteri" autocomplete="new-password" required>
<input type="password" id="password" name="password" class="form-control"
placeholder="Minimo 8 caratteri" autocomplete="new-password" required
aria-describedby="ps-text">
<button type="button" class="pw-toggle" tabindex="-1"
onclick="togglePw('password', this)" aria-label="Mostra/nascondi password">
<i class="fas fa-eye"></i>
onclick="togglePw('password', this)" aria-label="Mostra o nascondi la password">
<svg class="icon icon-sm" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-password-visible"></use></svg>
</button>
</div>
<div class="password-strength" id="password-strength">
<div class="password-strength-bar">
<div class="password-strength-segment" id="ps-1"></div>
<div class="password-strength-segment" id="ps-2"></div>
<div class="password-strength-segment" id="ps-3"></div>
<div class="password-strength-segment" id="ps-4"></div>
</div>
<div class="password-strength-text" id="ps-text"></div>
<div class="progress mt-2" style="height:6px;">
<div class="progress-bar" id="ps-bar" role="progressbar" style="width:0%;" aria-hidden="true"></div>
</div>
<div class="form-text" id="ps-text" aria-live="polite"></div>
</div>
<div class="form-group">
<label class="form-label" for="password-confirm">Conferma Password <span class="required">*</span></label>
<label for="password-confirm" class="active">Conferma Password <span aria-hidden="true">*</span></label>
<div class="pw-wrap">
<input type="password" id="password-confirm" name="password-confirm" class="form-input"
<input type="password" id="password-confirm" name="password-confirm" class="form-control"
placeholder="Ripeti la password" autocomplete="new-password" required>
<button type="button" class="pw-toggle" tabindex="-1"
onclick="togglePw('password-confirm', this)" aria-label="Mostra/nascondi password">
<i class="fas fa-eye"></i>
onclick="togglePw('password-confirm', this)" aria-label="Mostra o nascondi la password">
<svg class="icon icon-sm" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-password-visible"></use></svg>
</button>
</div>
</div>
<div style="display:flex; gap:.75rem;">
<button type="button" class="btn btn-secondary" onclick="goToStep0()" style="flex:0 0 auto;">
<i class="fas fa-arrow-left"></i>
<div class="d-flex" style="gap:.75rem;">
<button type="button" class="btn btn-outline-primary" onclick="goToStep0()" aria-label="Torna allo step precedente">
<svg class="ico" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-arrow-left"></use></svg>
</button>
<button type="submit" class="btn btn-primary btn-lg" id="register-btn" style="flex:1;">
<button type="submit" class="btn btn-primary flex-grow-1" id="register-btn">
Crea Account
</button>
</div>
@@ -311,79 +223,68 @@
<!-- STEP 2: Successo -->
<div id="step-2">
<div class="success-body">
<div class="success-icon"><i class="fas fa-check"></i></div>
<div class="success-title" id="success-title">Account creato!</div>
<div class="success-desc" id="success-desc">
Il tuo account NIS2 Agile è pronto. Stai per essere reindirizzato...
<div class="success-icon">
<svg class="icon icon-white" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-check"></use></svg>
</div>
<button class="btn btn-primary btn-lg w-full" id="success-btn" onclick="goToDashboard()">
Entra nella piattaforma <i class="fas fa-arrow-right" style="margin-left:.4rem;"></i>
<h2 class="h4" id="success-title">Account creato!</h2>
<p id="success-desc" class="text-muted">
Il tuo account NIS2 Agile è pronto. Stai per essere reindirizzato...
</p>
<button class="btn btn-primary w-100" id="success-btn" onclick="goToDashboard()">
Entra nella piattaforma
<svg class="ico ms-1" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-arrow-right"></use></svg>
</button>
</div>
</div>
</div>
<div class="auth-footer">
<p>Hai già un account? <a href="login.html">Accedi</a></p>
<div class="auth-terms">
Registrandoti accetti i nostri
<a href="https://agentai.agile.software/terms" target="_blank">Termini di Servizio</a>
e la <a href="https://agentai.agile.software/privacy" target="_blank">Privacy Policy</a>
</div>
</div>
</div>
</div>
<p class="text-center mt-3 mb-0">
Hai già un account? <a href="login.html">Accedi</a>
</p>
<p class="text-center small text-muted">
Registrandoti accetti i
<a href="https://agentai.agile.software/terms" target="_blank" rel="noopener">Termini di Servizio</a>
e la <a href="https://agentai.agile.software/privacy" target="_blank" rel="noopener">Privacy Policy</a>.
</p>
</main>
<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>
<!-- Stessa logica della register.html: ZERO modifiche backend -->
<script src="js/api.js"></script>
<script src="js/common.js"></script>
<script>
if (api.isAuthenticated()) {
window.location.href = 'dashboard.html';
}
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
// --- State ---
let selectedRole = null;
let inviteValid = false;
let inviteToken = null;
let inviteRecipient = null; // dati destinatario dell'invito (pre-fill)
let isReducedReg = false; // true = registrazione ridotta (solo password)
let inviteRecipient = null;
let isReducedReg = false;
let pivaData = null;
let pivaLookupTimer = null;
let successRedirect = 'onboarding.html';
// --- URL Params pre-fill ---
const params = new URLSearchParams(window.location.search);
if (params.get('role')) {
// Will apply after DOM is ready (in DOMContentLoaded equivalent below)
document.addEventListener('DOMContentLoaded', () => {
const r = params.get('role');
if (document.getElementById('card-' + r)) selectRole(r);
});
}
const _inviteTok = params.get('invite_token') || params.get('invite') || params.get('token');
if (_inviteTok) {
inviteToken = _inviteTok;
document.addEventListener('DOMContentLoaded', () => {
const details = document.getElementById('invite-details');
details.open = true;
const inp = document.getElementById('invite-code');
inp.value = inviteToken;
validateInvite(inviteToken);
});
}
// --- Role Selection ---
function selectRole(role) {
selectedRole = role;
document.querySelectorAll('.role-card').forEach(c => c.classList.remove('selected'));
document.querySelectorAll('.role-card').forEach(c => { c.classList.remove('selected'); c.setAttribute('aria-checked','false'); });
const card = document.getElementById('card-' + role);
if (card) card.classList.add('selected');
if (card) { card.classList.add('selected'); card.setAttribute('aria-checked','true'); }
document.getElementById('btn-next-0').disabled = false;
// Hide P.IVA for consultant role
const pivaGroup = document.getElementById('piva-group');
pivaGroup.style.display = (role === 'consultant') ? 'none' : 'block';
}
function cardKey(e, role) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectRole(role); }
}
// Pre-fill from URL after DOM ready
(function() {
@@ -396,7 +297,6 @@
document.getElementById('invite-code').value = tok;
validateInvite(tok);
}
// Pre-fill name/email
if (params.get('nome')) document.getElementById('firstname').value = params.get('nome');
if (params.get('cognome')) document.getElementById('lastname').value = params.get('cognome');
if (params.get('email')) document.getElementById('email').value = params.get('email');
@@ -409,13 +309,10 @@
clearTimeout(inviteTimer);
val = val.trim();
if (!val) {
inviteValid = false;
inviteToken = null;
statusEl.innerHTML = '';
return;
inviteValid = false; inviteToken = null; statusEl.innerHTML = ''; statusEl.className = 'lookup-status'; return;
}
statusEl.className = 'lookup-status loading';
statusEl.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Verifica codice...';
statusEl.textContent = 'Verifica codice...';
inviteTimer = setTimeout(async () => {
try {
const res = await fetch(api.baseUrl + '/auth/validate-invite', {
@@ -425,16 +322,11 @@
});
const data = await res.json();
if (data.success) {
inviteValid = true;
inviteToken = val;
inviteValid = true; inviteToken = val;
statusEl.className = 'lookup-status ok';
const label = data.data?.label ? ` — ${data.data.label}` : '';
statusEl.innerHTML = `<i class="fas fa-check-circle"></i> Codice valido${label} — accesso B2B confermato`;
// Pre-fill role if provided
if (data.role && document.getElementById('card-' + data.role)) {
selectRole(data.role);
}
// Pre-fill dai dati destinatario nell'invite (recipient)
statusEl.textContent = `Codice valido${label} — accesso B2B confermato`;
if (data.role && document.getElementById('card-' + data.role)) selectRole(data.role);
const r = data.data?.recipient;
if (r) {
if (r.first_name) document.getElementById('firstname').value = r.first_name;
@@ -446,23 +338,20 @@
}
if (isReducedReg) applyReducedRegMode();
} else {
inviteValid = false;
inviteToken = null;
inviteValid = false; inviteToken = null;
statusEl.className = 'lookup-status err';
statusEl.innerHTML = '<i class="fas fa-times-circle"></i> Codice non valido';
statusEl.textContent = 'Codice non valido';
}
} catch {
// Network error: accept token optimistically, validate on submit
inviteValid = false;
statusEl.className = 'lookup-status err';
statusEl.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Impossibile verificare';
statusEl.textContent = 'Impossibile verificare';
}
}, 600);
}
// --- P.IVA Lookup ---
function validaFormatoPiva(v) {
// Controllo cifra di controllo P.IVA italiana
if (!/^\d{11}$/.test(v)) return false;
let s = 0;
for (let i = 0; i < 10; i++) {
@@ -477,16 +366,15 @@
clearTimeout(pivaLookupTimer);
val = val.replace(/\s/g, '');
const statusEl = document.getElementById('piva-status');
if (val.length < 11) { pivaData = null; statusEl.innerHTML = ''; return; }
if (val.length < 11) { pivaData = null; statusEl.innerHTML = ''; statusEl.className = 'lookup-status'; return; }
if (val.length === 11 && !validaFormatoPiva(val)) {
pivaData = null;
statusEl.className = 'lookup-status err';
statusEl.innerHTML = '<i class="fas fa-times-circle"></i> P.IVA non valida (cifra di controllo errata)';
statusEl.textContent = 'P.IVA non valida (cifra di controllo errata)';
return;
}
// Formato OK — mostra subito conferma, poi prova lookup
statusEl.className = 'lookup-status ok';
statusEl.innerHTML = '<i class="fas fa-check-circle"></i> P.IVA valida — ricerca dati azienda...';
statusEl.textContent = 'P.IVA valida — ricerca dati azienda...';
pivaLookupTimer = setTimeout(async () => {
try {
const res = await fetch(api.baseUrl + '/onboarding/lookup-piva', {
@@ -498,40 +386,32 @@
if (data.success && data.data && data.data.company_name) {
pivaData = data.data;
statusEl.className = 'lookup-status ok';
statusEl.innerHTML = '<i class="fas fa-building"></i> ' + data.data.company_name;
statusEl.textContent = data.data.company_name;
} else {
pivaData = null;
statusEl.className = 'lookup-status ok';
statusEl.innerHTML = '<i class="fas fa-check-circle"></i> P.IVA valida — inserisci i dati aziendali nel passo successivo';
statusEl.textContent = 'P.IVA valida — inserisci i dati aziendali nel passo successivo';
}
} catch {
pivaData = null;
statusEl.className = 'lookup-status ok';
statusEl.innerHTML = '<i class="fas fa-check-circle"></i> P.IVA valida — inserisci i dati aziendali nel passo successivo';
statusEl.textContent = 'P.IVA valida — inserisci i dati aziendali nel passo successivo';
}
}, 800);
}
// --- Registrazione Ridotta (invito con dati pre-compilati) ---
// --- Registrazione Ridotta ---
function applyReducedRegMode() {
const name = ((inviteRecipient.first_name || '') + ' ' + (inviteRecipient.last_name || '')).trim();
document.getElementById('invite-welcome-name').textContent = 'Ciao ' + name + '!';
document.getElementById('invite-welcome-plan').textContent = 'Accesso B2B — account pre-configurato dall\'invito';
document.getElementById('invite-welcome').style.display = 'block';
// Campi pre-compilati → read-only
['firstname', 'lastname', 'email'].forEach(id => {
const el = document.getElementById(id);
el.readOnly = true;
el.style.background = '#f9fafb';
el.style.color = '#6b7280';
});
// Nascondi P.IVA se già presente nell'invito
if (inviteRecipient.vat) {
document.getElementById('piva-group').style.display = 'none';
}
// Seleziona ruolo default se non già selezionato
if (inviteRecipient.vat) document.getElementById('piva-group').style.display = 'none';
if (!selectedRole) selectRole('org_admin');
// Avanza direttamente allo step dati senza richiedere click
goToStep1();
}
@@ -541,18 +421,17 @@
document.getElementById('step-' + i).style.display = (i === n) ? 'block' : 'none';
const si = document.getElementById('si-' + i);
si.className = 'step-item' + (i < n ? ' done' : i === n ? ' active' : '');
if (i < n) si.querySelector('.step-circle').innerHTML = '<i class="fas fa-check" style="font-size:.75rem;"></i>';
if (i < n) si.querySelector('.step-circle').innerHTML = '<svg class="icon icon-xs icon-white" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-check"></use></svg>';
else si.querySelector('.step-circle').textContent = i + 1;
});
document.getElementById('conn-0').className = 'step-connector' + (n > 0 ? ' done' : '');
document.getElementById('conn-1').className = 'step-connector' + (n > 1 ? ' done' : '');
if (n === 2) document.getElementById('step-indicator').style.display = 'none';
else document.getElementById('step-indicator').style.display = 'flex';
document.getElementById('step-indicator').style.display = (n === 2) ? 'none' : 'flex';
}
function goToStep1() {
if (!selectedRole) return;
document.getElementById('register-error').classList.remove('visible');
document.getElementById('register-error').classList.add('d-none');
const labels = {
compliance_manager: 'Dati account — CISO / Compliance Manager',
org_admin: 'Dati account — Legale Rappresentante',
@@ -566,25 +445,18 @@
}
function goToStep0() {
document.getElementById('register-error').classList.remove('visible');
document.getElementById('register-error').classList.add('d-none');
document.getElementById('auth-subtitle').textContent = 'Crea il tuo account';
setStep(0);
}
function goToDashboard() {
window.location.href = successRedirect;
}
function goToDashboard() { window.location.href = successRedirect; }
// --- Password toggle ---
function togglePw(id, btn) {
const inp = document.getElementById(id);
if (inp.type === 'password') {
inp.type = 'text';
btn.innerHTML = '<i class="fas fa-eye-slash"></i>';
} else {
inp.type = 'password';
btn.innerHTML = '<i class="fas fa-eye"></i>';
}
inp.type = (inp.type === 'password') ? 'text' : 'password';
const use = btn.querySelector('use');
use.setAttribute('href', 'vendor/bootstrap-italia/dist/svg/sprites.svg#' + (inp.type === 'text' ? 'it-password-invisible' : 'it-password-visible'));
}
// --- Password strength ---
@@ -604,20 +476,19 @@
function updateStrengthUI(level, pw) {
const labels = { 1: 'Debole', 2: 'Sufficiente', 3: 'Buona', 4: 'Forte' };
const classes = { 1: 'weak', 2: 'fair', 3: 'good', 4: 'strong' };
for (let i = 1; i <= 4; i++) {
const seg = document.getElementById('ps-' + i);
seg.className = 'password-strength-segment';
if (i <= level && pw.length > 0) seg.classList.add('active', classes[level]);
}
document.getElementById('ps-text').textContent = pw.length > 0 ? labels[level] : '';
const colors = { 1: '#d9364f', 2: '#e58a00', 3: '#06c', 4: '#008758' };
const bar = document.getElementById('ps-bar');
if (pw.length === 0) { bar.style.width = '0%'; document.getElementById('ps-text').textContent = ''; return; }
bar.style.width = (level * 25) + '%';
bar.style.backgroundColor = colors[level];
document.getElementById('ps-text').textContent = 'Sicurezza: ' + labels[level];
}
// --- Form Submit ---
document.getElementById('register-form').addEventListener('submit', async (e) => {
e.preventDefault();
const errorEl = document.getElementById('register-error');
errorEl.classList.remove('visible');
errorEl.classList.add('d-none');
const firstname = document.getElementById('firstname').value.trim();
const lastname = document.getElementById('lastname').value.trim();
@@ -627,26 +498,12 @@
const passwordConfirm = document.getElementById('password-confirm').value;
const fullname = firstname + ' ' + lastname;
if (!firstname || !lastname || !email || !password) {
errorEl.textContent = 'Nome, cognome, email e password sono obbligatori.';
errorEl.classList.add('visible');
return;
}
if (selectedRole !== 'consultant' && !piva) {
errorEl.textContent = 'Inserisci la Partita IVA della tua azienda.';
errorEl.classList.add('visible');
return;
}
if (password.length < 8) {
errorEl.textContent = 'La password deve avere almeno 8 caratteri.';
errorEl.classList.add('visible');
return;
}
if (password !== passwordConfirm) {
errorEl.textContent = 'Le password non coincidono.';
errorEl.classList.add('visible');
return;
}
function showErr(msg) { errorEl.textContent = msg; errorEl.classList.remove('d-none'); }
if (!firstname || !lastname || !email || !password) { showErr('Nome, cognome, email e password sono obbligatori.'); return; }
if (selectedRole !== 'consultant' && !piva) { showErr('Inserisci la Partita IVA della tua azienda.'); return; }
if (password.length < 8) { showErr('La password deve avere almeno 8 caratteri.'); return; }
if (password !== passwordConfirm) { showErr('Le password non coincidono.'); return; }
const btn = document.getElementById('register-btn');
btn.disabled = true;
@@ -654,9 +511,7 @@
try {
let result;
if (inviteToken) {
// B2B provision flow
result = await fetch(api.baseUrl + '/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -672,15 +527,12 @@
}
if (result.success) {
// Determine redirect
if (inviteToken) {
successRedirect = 'dashboard.html';
// Salva JWT tokens
if (result.data?.access_token) {
api.setTokens(result.data.access_token, result.data.refresh_token);
localStorage.setItem('nis2_user_role', result.data.user?.role || selectedRole || 'org_admin');
}
// Chiama provision per creare l'organizzazione
try {
const companyName = pivaData?.company_name || inviteRecipient?.company || 'Azienda';
const vatNum = (piva || inviteRecipient?.vat || '').replace(/[^0-9]/g, '');
@@ -706,14 +558,13 @@
localStorage.setItem('nis2_org_id', String(provData.data.org_id));
}
}
} catch { /* provision fallita — utente completerà onboarding */ }
} catch { /* provision fallita */ }
} else if (selectedRole === 'consultant') {
successRedirect = 'companies.html';
} else {
successRedirect = 'onboarding.html';
}
// Success step
const titles = {
consultant: 'Account Consulente creato!',
board_member: 'Account Board Member creato!',
@@ -729,12 +580,10 @@
setStep(2);
setTimeout(goToDashboard, 2500);
} else {
errorEl.textContent = result.message || 'Errore durante la registrazione.';
errorEl.classList.add('visible');
showErr(result.message || 'Errore durante la registrazione.');
}
} catch (err) {
errorEl.textContent = 'Errore di connessione al server.';
errorEl.classList.add('visible');
showErr('Errore di connessione al server.');
} finally {
btn.disabled = false;
btn.textContent = 'Crea Account';
+15 -1
View File
@@ -4,6 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Audit e Report - NIS2 Agile</title>
<!-- 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 (tabelle/modali/form) -> 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>
/* ── Tab Navigation ─────────────────────────────────────── */
@@ -346,7 +350,7 @@
<main class="main-content">
<header class="content-header">
<h2 data-i18n="audit.title">Audit &amp; Report</h2>
<h1 class="h2" data-i18n="audit.title">Audit &amp; Report</h1>
<div class="content-header-actions">
<button class="btn btn-primary" onclick="generateReport()">Genera Report</button>
</div>
@@ -428,6 +432,16 @@
<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 src="/js/help.js"></script>
<script>
+67 -63
View File
@@ -4,84 +4,86 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Imposta nuova password - 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 da /vendor (MAI CDN). Variante "-bi" della reset-password.html. -->
<link rel="stylesheet" href="vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<style>
body { min-height: 100vh; display: flex; align-items: center; background: #f5f6f7; }
.auth-wrap { width: 100%; max-width: 460px; margin: 0 auto; padding: 24px; }
.auth-brand { text-align: center; margin-bottom: 24px; }
.auth-brand .brand-name { font-weight: 700; font-size: 1.5rem; color: #06c; }
.auth-brand .brand-name span { color: #17324d; }
.auth-brand .brand-sub { color: #5a6772; font-size: .9rem; margin-top: 4px; }
.pw-wrap { position: relative; }
.pw-wrap .form-input { padding-right: 42px; }
.pw-toggle {
position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
background: none; border: none; cursor: pointer;
color: #9CA3AF; font-size: 15px; padding: 0;
}
.strength-bar { display:flex; gap:3px; margin-top:6px; }
.strength-bar span { flex:1; height:4px; border-radius:2px; background:#E0E4E8; transition:background .2s; }
.strength-text { font-size:.78rem; color:#6B7280; margin-top:6px; min-height:16px; }
.auth-success { background:#ECFDF5; color:#065F46; border:1px solid #A7F3D0; padding:12px 16px; border-radius:6px; font-size:.9rem; margin-bottom:16px; display:none; }
.auth-success.visible { display:block; }
.pw-rules { font-size:.78rem; color:#6B7280; margin-top:8px; line-height:1.6; }
.back-link { display:block; text-align:center; margin-top:14px; font-size:.85rem; color:#6B7280; text-decoration:none; }
.back-link:hover { color: var(--color-primary, #2563eb); }
.pw-wrap .form-control { padding-right: 44px; }
.pw-toggle { position: absolute; right: 8px; top: 8px; background: none; border: none; color: #5a6772; cursor: pointer; padding: 6px; }
.ico { width: 1em; height: 1em; fill: currentColor; vertical-align: -.125em; }
</style>
</head>
<body>
<div class="auth-page">
<div class="auth-card">
<div class="auth-header">
<div class="auth-logo">
<div class="auth-logo-icon">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 2.18l7 3.12v4.7c0 4.83-3.23 9.36-7 10.57-3.77-1.21-7-5.74-7-10.57V6.3l7-3.12z"/>
</svg>
</div>
<span class="auth-logo-text">NIS2 <span>Agile</span></span>
</div>
<p class="auth-subtitle">Imposta una nuova password</p>
</div>
<main class="auth-wrap">
<div class="auth-brand">
<div class="brand-name">NIS2 <span>Agile</span></div>
<div class="brand-sub">Imposta una nuova password</div>
</div>
<div class="auth-body">
<div class="auth-error" id="err"></div>
<div class="auth-success" id="ok"></div>
<div class="card card-bg has-bkg-grey shadow">
<div class="card-body">
<h1 class="h4 mb-3">Nuova password</h1>
<div class="alert alert-danger d-none" id="err" role="alert"></div>
<div class="alert alert-success d-none" id="ok" role="status"></div>
<form id="reset-form" novalidate>
<div class="form-group">
<label class="form-label" for="pw">Nuova password</label>
<label for="pw" class="active">Nuova password</label>
<div class="pw-wrap">
<input type="password" id="pw" class="form-input"
placeholder="Almeno 8 caratteri" autocomplete="new-password" required>
<button type="button" class="pw-toggle" onclick="togglePw('pw',this)" aria-label="Mostra/nascondi password">
<i class="fas fa-eye"></i>
<input type="password" id="pw" class="form-control"
placeholder="Almeno 8 caratteri" autocomplete="new-password" required
aria-describedby="strength-text">
<button type="button" class="pw-toggle" tabindex="-1" onclick="togglePw('pw',this)" aria-label="Mostra o nascondi la password">
<svg class="icon icon-sm" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-password-visible"></use></svg>
</button>
</div>
<div class="strength-bar"><span></span><span></span><span></span><span></span></div>
<div class="strength-text" id="strength-text"></div>
<div class="progress mt-2" style="height:4px;">
<div class="progress-bar" id="strength-bar" role="progressbar" style="width:0%;" aria-hidden="true"></div>
</div>
<div class="form-text" id="strength-text" aria-live="polite"></div>
</div>
<div class="form-group">
<label class="form-label" for="pw2">Conferma password</label>
<label for="pw2" class="active">Conferma password</label>
<div class="pw-wrap">
<input type="password" id="pw2" class="form-input"
<input type="password" id="pw2" class="form-control"
placeholder="Ripeti la password" autocomplete="new-password" required>
<button type="button" class="pw-toggle" onclick="togglePw('pw2',this)" aria-label="Mostra/nascondi password">
<i class="fas fa-eye"></i>
<button type="button" class="pw-toggle" tabindex="-1" onclick="togglePw('pw2',this)" aria-label="Mostra o nascondi la password">
<svg class="icon icon-sm" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-password-visible"></use></svg>
</button>
</div>
</div>
<p class="pw-rules">Minimo 8 caratteri, con almeno una maiuscola, una minuscola e un numero.</p>
<p class="text-muted small mb-3">Minimo 8 caratteri, con almeno una maiuscola, una minuscola e un numero.</p>
<button type="submit" class="btn btn-primary btn-lg w-full" id="submit-btn" style="margin-top:12px;">
<button type="submit" class="btn btn-primary w-100" id="submit-btn">
Imposta password
</button>
</form>
</div>
<div class="auth-footer">
<a href="login.html" class="back-link"><i class="fas fa-arrow-left"></i> Torna al login</a>
</div>
</div>
</div>
<p class="text-center mt-3 mb-0">
<a href="login.html">
<svg class="ico me-1" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-arrow-left"></use></svg>
Torna al login
</a>
</p>
</main>
<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>
<!-- Stessa logica della reset-password.html: ZERO modifiche backend -->
<script>
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token') || '';
@@ -90,19 +92,20 @@
const form = document.getElementById('reset-form');
const btn = document.getElementById('submit-btn');
const pwInput = document.getElementById('pw');
const pwBars = document.querySelectorAll('.strength-bar span');
const pwBar = document.getElementById('strength-bar');
const pwText = document.getElementById('strength-text');
if (!token) {
err.textContent = 'Token mancante. Richiedi un nuovo link di reset.';
err.classList.add('visible');
err.classList.remove('d-none');
form.style.display = 'none';
}
function togglePw(id, button) {
const i = document.getElementById(id);
if (i.type === 'password') { i.type = 'text'; button.innerHTML = '<i class="fas fa-eye-slash"></i>'; }
else { i.type = 'password'; button.innerHTML = '<i class="fas fa-eye"></i>'; }
i.type = (i.type === 'password') ? 'text' : 'password';
const use = button.querySelector('use');
use.setAttribute('href', 'vendor/bootstrap-italia/dist/svg/sprites.svg#' + (i.type === 'text' ? 'it-password-invisible' : 'it-password-visible'));
}
function computeStrength(pw) {
@@ -116,27 +119,28 @@
pwInput.addEventListener('input', function() {
const s = computeStrength(pwInput.value);
const colors = ['#E0E4E8', '#EF4444', '#F59E0B', '#3B82F6', '#10B981'];
const colors = ['#d4d8db', '#d9364f', '#e58a00', '#06c', '#008758'];
const labels = ['', 'Debole', 'Media', 'Buona', 'Ottima'];
pwBars.forEach(function(bar, i) { bar.style.background = (i < s) ? colors[s] : '#E0E4E8'; });
pwBar.style.width = (pwInput.value.length > 0 ? (s * 25) : 0) + '%';
pwBar.style.backgroundColor = colors[s];
pwText.textContent = pwInput.value.length > 0 ? 'Sicurezza: ' + labels[s] : '';
});
form.addEventListener('submit', async function(e) {
e.preventDefault();
err.classList.remove('visible');
ok.classList.remove('visible');
err.classList.add('d-none');
ok.classList.add('d-none');
const pw = document.getElementById('pw').value;
const pw2 = document.getElementById('pw2').value;
if (pw !== pw2) {
err.textContent = 'Le due password non coincidono.';
err.classList.add('visible');
err.classList.remove('d-none');
return;
}
if (pw.length < 8) {
err.textContent = 'La password deve essere di almeno 8 caratteri.';
err.classList.add('visible');
err.classList.remove('d-none');
return;
}
@@ -151,16 +155,16 @@
const data = await res.json();
if (data.success) {
ok.textContent = data.message + ' Sarai reindirizzato al login.';
ok.classList.add('visible');
ok.classList.remove('d-none');
form.style.display = 'none';
setTimeout(function() { window.location.href = 'login.html'; }, 2500);
} else {
err.textContent = data.message || 'Errore. Token non valido o scaduto.';
err.classList.add('visible');
err.classList.remove('d-none');
}
} catch (e) {
err.textContent = 'Errore di connessione al server.';
err.classList.add('visible');
err.classList.remove('d-none');
} finally {
btn.disabled = false;
btn.textContent = 'Imposta password';
+60 -46
View File
@@ -4,6 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gestione Rischi - NIS2 Agile</title>
<!-- 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 (tabelle/matrice/modali) -> 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>
/* ── View Toggle ─────────────────────────────────────── */
@@ -301,7 +305,7 @@
<main class="main-content">
<header class="content-header">
<h2 data-i18n="risks.title">Gestione Rischi</h2>
<h1 class="h2" data-i18n="risks.title">Gestione Rischi</h1>
<div class="content-header-actions">
<div class="view-toggle" id="view-toggle">
<button class="active" onclick="switchView('table')">Tabella</button>
@@ -321,7 +325,7 @@
<!-- Table View -->
<div id="view-table">
<div class="filters-bar">
<select class="form-select" id="filter-category" onchange="loadRisks()">
<select class="form-select" id="filter-category" onchange="loadRisks()" aria-label="Filtra per categoria">
<option value="">Tutte le categorie</option>
<option value="cyber">Cyber</option>
<option value="operational">Operativo</option>
@@ -330,7 +334,7 @@
<option value="physical">Fisico</option>
<option value="human">Umano</option>
</select>
<select class="form-select" id="filter-status" onchange="loadRisks()">
<select class="form-select" id="filter-status" onchange="loadRisks()" aria-label="Filtra per stato">
<option value="">Tutti gli stati</option>
<option value="identified">Identificato</option>
<option value="analyzing">In Analisi</option>
@@ -345,14 +349,14 @@
<table class="table-clickable">
<thead>
<tr>
<th>Codice</th>
<th>Titolo</th>
<th>Categoria</th>
<th>Probabilita'</th>
<th>Impatto</th>
<th>Score</th>
<th>Stato</th>
<th>Azioni</th>
<th scope="col">Codice</th>
<th scope="col">Titolo</th>
<th scope="col">Categoria</th>
<th scope="col">Probabilita'</th>
<th scope="col">Impatto</th>
<th scope="col">Score</th>
<th scope="col">Stato</th>
<th scope="col">Azioni</th>
</tr>
</thead>
<tbody id="risks-table-body">
@@ -421,15 +425,15 @@
</div>
<div class="card-body">
<div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr)); gap:12px; margin-bottom:16px;">
<div><label class="form-label">Rischio</label>
<div><label for="fair-risk-select" class="form-label">Rischio</label>
<select class="form-select" id="fair-risk-select"><option value="">— seleziona —</option></select></div>
<div><label class="form-label">TEF min <small>(ev/anno)</small></label><input type="number" step="0.1" class="form-input" id="fair-tef-min" value="0.5"></div>
<div><label class="form-label">TEF probabile</label><input type="number" step="0.1" class="form-input" id="fair-tef-ml" value="1"></div>
<div><label class="form-label">TEF max</label><input type="number" step="0.1" class="form-input" id="fair-tef-max" value="2"></div>
<div><label class="form-label">Vulnerabilita <small>(0-1)</small></label><input type="number" step="0.05" min="0" max="1" class="form-input" id="fair-vuln" value="0.3"></div>
<div><label class="form-label">Perdita min <small>(EUR)</small></label><input type="number" step="1000" class="form-input" id="fair-lm-min" value="50000"></div>
<div><label class="form-label">Perdita probabile</label><input type="number" step="1000" class="form-input" id="fair-lm-ml" value="300000"></div>
<div><label class="form-label">Perdita max</label><input type="number" step="1000" class="form-input" id="fair-lm-max" value="2000000"></div>
<div><label for="fair-tef-min" class="form-label">TEF min <small>(ev/anno)</small></label><input type="number" step="0.1" class="form-input" id="fair-tef-min" value="0.5"></div>
<div><label for="fair-tef-ml" class="form-label">TEF probabile</label><input type="number" step="0.1" class="form-input" id="fair-tef-ml" value="1"></div>
<div><label for="fair-tef-max" class="form-label">TEF max</label><input type="number" step="0.1" class="form-input" id="fair-tef-max" value="2"></div>
<div><label for="fair-vuln" class="form-label">Vulnerabilita <small>(0-1)</small></label><input type="number" step="0.05" min="0" max="1" class="form-input" id="fair-vuln" value="0.3"></div>
<div><label for="fair-lm-min" class="form-label">Perdita min <small>(EUR)</small></label><input type="number" step="1000" class="form-input" id="fair-lm-min" value="50000"></div>
<div><label for="fair-lm-ml" class="form-label">Perdita probabile</label><input type="number" step="1000" class="form-input" id="fair-lm-ml" value="300000"></div>
<div><label for="fair-lm-max" class="form-label">Perdita max</label><input type="number" step="1000" class="form-input" id="fair-lm-max" value="2000000"></div>
</div>
<button class="btn btn-primary" onclick="runFair()" id="btn-run-fair">Calcola ALE</button>
<div id="fair-result" class="hidden" style="margin-top:20px;">
@@ -450,7 +454,7 @@
<span class="text-muted" style="font-size:0.8rem;">ALE di portafoglio: <strong id="fair-portfolio">—</strong></span></div>
<div class="table-container">
<table class="table-clickable"><thead><tr>
<th>Codice</th><th>Titolo</th><th>Categoria</th><th>ALE Min</th><th>ALE Mediana</th><th>ALE Max</th><th>ALE Media</th>
<th scope="col">Codice</th><th scope="col">Titolo</th><th scope="col">Categoria</th><th scope="col">ALE Min</th><th scope="col">ALE Mediana</th><th scope="col">ALE Max</th><th scope="col">ALE Media</th>
</tr></thead><tbody id="fair-register-body"><tr><td colspan="7" class="text-muted" style="text-align:center;padding:20px;">Nessun rischio quantificato</td></tr></tbody></table>
</div>
</div>
@@ -465,7 +469,7 @@
<div class="card">
<div class="table-container">
<table class="table-clickable"><thead><tr>
<th>Stato</th><th>Indicatore</th><th>Categoria</th><th>Valore</th><th>Soglia Warning</th><th>Soglia Critica</th><th>Azioni</th>
<th scope="col">Stato</th><th scope="col">Indicatore</th><th scope="col">Categoria</th><th scope="col">Valore</th><th scope="col">Soglia Warning</th><th scope="col">Soglia Critica</th><th scope="col">Azioni</th>
</tr></thead><tbody id="kri-table-body"><tr><td colspan="7"><div class="spinner" style="margin:30px auto;"></div></td></tr></tbody></table>
</div>
</div>
@@ -479,6 +483,16 @@
<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 src="/js/help.js"></script>
<script>
@@ -747,17 +761,17 @@
function showKriModal(kri = null) {
const k = kri || {};
const body = `
<div class="form-group"><label class="form-label">Nome indicatore *</label><input class="form-input" id="kri-name" value="${escapeHtml(k.name || '')}"></div>
<div class="form-group"><label class="form-label">Descrizione</label><input class="form-input" id="kri-desc" value="${escapeHtml(k.description || '')}"></div>
<div class="form-group"><label for="kri-name" class="form-label">Nome indicatore *</label><input class="form-input" id="kri-name" value="${escapeHtml(k.name || '')}"></div>
<div class="form-group"><label for="kri-desc" class="form-label">Descrizione</label><input class="form-input" id="kri-desc" value="${escapeHtml(k.description || '')}"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<div class="form-group"><label class="form-label">Categoria</label><select class="form-select" id="kri-cat">
<div class="form-group"><label for="kri-cat" class="form-label">Categoria</label><select class="form-select" id="kri-cat">
${['cyber', 'operational', 'compliance', 'supply_chain', 'physical', 'human'].map(c => `<option value="${c}" ${k.category === c ? 'selected' : ''}>${CATEGORY_LABELS[c] || c}</option>`).join('')}</select></div>
<div class="form-group"><label class="form-label">Unita (%, count, EUR...)</label><input class="form-input" id="kri-unit" value="${escapeHtml(k.unit || '')}"></div>
<div class="form-group"><label class="form-label">Valore corrente</label><input type="number" step="any" class="form-input" id="kri-cur" value="${k.current_value ?? ''}"></div>
<div class="form-group"><label class="form-label">Obiettivo</label><input type="number" step="any" class="form-input" id="kri-target" value="${k.target_value ?? ''}"></div>
<div class="form-group"><label class="form-label">Soglia Warning</label><input type="number" step="any" class="form-input" id="kri-warn" value="${k.threshold_warning ?? ''}"></div>
<div class="form-group"><label class="form-label">Soglia Critica</label><input type="number" step="any" class="form-input" id="kri-crit" value="${k.threshold_critical ?? ''}"></div>
<div class="form-group" style="grid-column:span 2;"><label class="form-label">Direzione</label><select class="form-select" id="kri-dir">
<div class="form-group"><label for="kri-unit" class="form-label">Unita (%, count, EUR...)</label><input class="form-input" id="kri-unit" value="${escapeHtml(k.unit || '')}"></div>
<div class="form-group"><label for="kri-cur" class="form-label">Valore corrente</label><input type="number" step="any" class="form-input" id="kri-cur" value="${k.current_value ?? ''}"></div>
<div class="form-group"><label for="kri-target" class="form-label">Obiettivo</label><input type="number" step="any" class="form-input" id="kri-target" value="${k.target_value ?? ''}"></div>
<div class="form-group"><label for="kri-warn" class="form-label">Soglia Warning</label><input type="number" step="any" class="form-input" id="kri-warn" value="${k.threshold_warning ?? ''}"></div>
<div class="form-group"><label for="kri-crit" class="form-label">Soglia Critica</label><input type="number" step="any" class="form-input" id="kri-crit" value="${k.threshold_critical ?? ''}"></div>
<div class="form-group" style="grid-column:span 2;"><label for="kri-dir" class="form-label">Direzione</label><select class="form-select" id="kri-dir">
<option value="higher_worse" ${k.direction === 'higher_worse' ? 'selected' : ''}>Valori alti = peggio</option>
<option value="lower_worse" ${k.direction === 'lower_worse' ? 'selected' : ''}>Valori bassi = peggio</option></select></div>
</div>`;
@@ -1065,16 +1079,16 @@
const content = `
<div class="form-group">
<label class="form-label">Titolo <span class="required">*</span></label>
<label for="risk-title" class="form-label">Titolo <span class="required">*</span></label>
<input type="text" class="form-input" id="risk-title" value="${escapeHtml(risk.title || '')}" placeholder="Titolo del rischio">
</div>
<div class="form-group">
<label class="form-label">Descrizione</label>
<label for="risk-description" class="form-label">Descrizione</label>
<textarea class="form-textarea" id="risk-description" rows="3" placeholder="Descrizione dettagliata">${escapeHtml(risk.description || '')}</textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Categoria</label>
<label for="risk-category" class="form-label">Categoria</label>
<select class="form-select" id="risk-category">
<option value="cyber" ${risk.category==='cyber'?'selected':''}>Cyber</option>
<option value="operational" ${risk.category==='operational'?'selected':''}>Operativo</option>
@@ -1085,7 +1099,7 @@
</select>
</div>
<div class="form-group">
<label class="form-label">Trattamento</label>
<label for="risk-treatment" class="form-label">Trattamento</label>
<select class="form-select" id="risk-treatment">
<option value="mitigate" ${risk.treatment==='mitigate'?'selected':''}>Mitigare</option>
<option value="accept" ${risk.treatment==='accept'?'selected':''}>Accettare</option>
@@ -1096,24 +1110,24 @@
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Sorgente Minaccia</label>
<label for="risk-threat-source" class="form-label">Sorgente Minaccia</label>
<input type="text" class="form-input" id="risk-threat-source" value="${escapeHtml(risk.threat_source || '')}" placeholder="Es. attaccante esterno">
</div>
<div class="form-group">
<label class="form-label">Vulnerabilita'</label>
<label for="risk-vulnerability" class="form-label">Vulnerabilita'</label>
<input type="text" class="form-input" id="risk-vulnerability" value="${escapeHtml(risk.vulnerability || '')}" placeholder="Es. software non aggiornato">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Probabilita' <span style="color:var(--gray-400); font-size:0.75rem; font-weight:400;">(frequenza stimata annua)</span></label>
<label for="risk-likelihood" class="form-label">Probabilita' <span style="color:var(--gray-400); font-size:0.75rem; font-weight:400;">(frequenza stimata annua)</span></label>
<div class="range-value-display" id="likelihood-value">${likelihood}</div>
<input type="range" class="form-range" id="risk-likelihood" min="1" max="5" value="${likelihood}" oninput="updateRiskScore()">
<div style="font-size:0.75rem; color:var(--gray-500); margin-top:4px; padding:6px 8px; background:var(--gray-50); border-radius:4px;" id="likelihood-detail">${LIKELIHOOD_DETAILS[likelihood]}</div>
</div>
<div class="form-group">
<label class="form-label">Impatto <span style="color:var(--gray-400); font-size:0.75rem; font-weight:400;">(danno economico / utenti)</span></label>
<label for="risk-impact" class="form-label">Impatto <span style="color:var(--gray-400); font-size:0.75rem; font-weight:400;">(danno economico / utenti)</span></label>
<div class="range-value-display" id="impact-value">${impact}</div>
<input type="range" class="form-range" id="risk-impact" min="1" max="5" value="${impact}" oninput="updateRiskScore()">
<div style="font-size:0.75rem; color:var(--gray-500); margin-top:4px; padding:6px 8px; background:var(--gray-50); border-radius:4px;" id="impact-detail">${IMPACT_DETAILS[impact]}</div>
@@ -1127,16 +1141,16 @@
<div class="form-row" style="margin-top:16px;">
<div class="form-group">
<label class="form-label">Responsabile (ID utente)</label>
<label for="risk-owner" class="form-label">Responsabile (ID utente)</label>
<input type="text" class="form-input" id="risk-owner" value="${escapeHtml(risk.owner_user_id || '')}" placeholder="ID utente responsabile">
</div>
<div class="form-group">
<label class="form-label">Data Revisione</label>
<label for="risk-review-date" class="form-label">Data Revisione</label>
<input type="date" class="form-input" id="risk-review-date" value="${risk.review_date ? risk.review_date.split(' ')[0] : ''}">
</div>
</div>
<div class="form-group">
<label class="form-label">Articolo NIS2 di riferimento</label>
<label for="risk-nis2-article" class="form-label">Articolo NIS2 di riferimento</label>
<input type="text" class="form-input" id="risk-nis2-article" value="${escapeHtml(risk.nis2_article || '')}" placeholder="Es. Art. 21, par. 2">
</div>
`;
@@ -1249,21 +1263,21 @@
function showAddTreatmentModal(riskId) {
const content = `
<div class="form-group">
<label class="form-label">Descrizione Azione <span class="required">*</span></label>
<label for="treatment-description" class="form-label">Descrizione Azione <span class="required">*</span></label>
<textarea class="form-textarea" id="treatment-description" rows="3" placeholder="Descrizione dell'azione di trattamento"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Responsabile (ID utente)</label>
<label for="treatment-responsible" class="form-label">Responsabile (ID utente)</label>
<input type="text" class="form-input" id="treatment-responsible" placeholder="ID utente">
</div>
<div class="form-group">
<label class="form-label">Data Scadenza</label>
<label for="treatment-due-date" class="form-label">Data Scadenza</label>
<input type="date" class="form-input" id="treatment-due-date">
</div>
</div>
<div class="form-group">
<label class="form-label">Stato</label>
<label for="treatment-status" class="form-label">Stato</label>
<select class="form-select" id="treatment-status">
<option value="planned">Pianificato</option>
<option value="in_progress">In Corso</option>
@@ -1271,7 +1285,7 @@
</select>
</div>
<div class="form-group">
<label class="form-label">Note</label>
<label for="treatment-notes" class="form-label">Note</label>
<textarea class="form-textarea" id="treatment-notes" rows="2" placeholder="Note aggiuntive"></textarea>
</div>
`;
+81 -56
View File
@@ -4,98 +4,105 @@
<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">
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.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}
body{font-family:'Titillium Web','Segoe UI',-apple-system,sans-serif;font-size:14.5px;line-height:1.7;color:#1A1A2E;background:#F5F7FA}
.icon-inline{vertical-align:-0.15em;margin-right:6px}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.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}
.doc-header{background:linear-gradient(135deg,#0D1B2A 0%,#06c 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-icon{width:44px;height:44px;border-radius:10px;background:rgba(255,255,255,.12);display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.85)}
.brand-icon .icon{fill:currentColor}
.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}
.brand-sub{font-size:.68rem;opacity:.6}
.doc-meta{text-align:right;font-size:.75rem;opacity:.6}
.doc-meta strong{display:block;color:rgba(255,255,255,.9);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}
.doc-title span{color:rgba(255,255,255,.85)}
.doc-sub{font-size:.84rem;opacity:.7}
.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)}
.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,.12);border:1px solid rgba(255,255,255,.2);color:#fff}
.tag .icon{fill:currentColor}
.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{font-size:1.25rem;font-weight:800;color:#06c;margin:36px 0 14px;padding-bottom:6px;border-bottom:2px solid #eee;display:flex;align-items:center;gap:8px}
h2:first-child{margin-top:0}
h2 i{margin-right:8px}
h3{font-size:1rem;font-weight:700;color:#1565C0;margin:22px 0 8px}
h2 .icon{fill:currentColor;flex:none}
h3{font-size:1rem;font-weight:700;color:#06c;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}
.info-box{background:#E8F5E9;border-left:4px solid #2E7D32;padding:14px 18px;border-radius:0 8px 8px 0;margin:16px 0;font-size:.88rem;display:flex;gap:8px;align-items:flex-start}
.info-box .icon{fill:#2E7D32;flex:none;margin-top:2px}
.warn-box{background:#FFF3E0;border-left:4px solid #F57F17;padding:14px 18px;border-radius:0 8px 8px 0;margin:16px 0;font-size:.88rem;display:flex;gap:8px;align-items:flex-start}
.warn-box .icon{fill:#F57F17;flex:none;margin-top:2px}
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}
code{background:#f5f5f5;padding:2px 6px;border-radius:4px;font-size:.8rem;font-family:'Roboto Mono','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-num{background:#06c;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}
.layer-body h4{margin:0 0 6px;color:#06c;font-size:.95rem;display:flex;align-items:center;gap:6px}
.layer-body h4 .icon{fill:currentColor;flex:none}
.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}
a{color:#06c}
:focus-visible{outline:3px solid #06c;outline-offset:2px}
@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">
<header class="doc-header">
<div class="header-top">
<div class="brand">
<div class="brand-icon"><i class="fas fa-shield-halved"></i></div>
<div class="brand-icon"><svg class="icon icon-white" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-locked"></use></svg></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>
<h1 class="doc-title">Strategia di <span>Continuita di Servizio</span></h1>
<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>
<span class="tag"><svg class="icon icon-xs icon-white" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-calendar"></use></svg> Aprile 2026</span>
<span class="tag"><svg class="icon icon-xs icon-white" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-software"></use></svg> PHP+MySQL+Qdrant</span>
<span class="tag"><svg class="icon icon-xs icon-white" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-pa"></use></svg> Hetzner CPX31</span>
<span class="tag"><svg class="icon icon-xs icon-white" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-chart-line"></use></svg> 99.9% SLA Target</span>
</div>
</div>
<div class="content">
</header>
<main class="content">
<h2><i class="fas fa-sitemap"></i> 1. Architettura di Servizio</h2>
<h2><svg class="icon icon-primary" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-pa"></use></svg> 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>
<table><caption class="sr-only">Servizi, stack tecnologico e healthcheck</caption><thead><tr><th scope="col">Servizio</th><th scope="col">Stack</th><th scope="col">Healthcheck</th></tr></thead><tbody><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></tbody></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.
<svg class="icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-info-circle"></use></svg>
<div><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>
</div>
<h2><i class="fas fa-layer-group"></i> 2. Strategia di Resilienza a 7 Livelli</h2>
<h2><svg class="icon icon-primary" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-files"></use></svg> 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>
<h4><svg class="icon icon-sm icon-primary" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-refresh"></use></svg> 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>
<h4><svg class="icon icon-sm icon-primary" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-chart-line"></use></svg> 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>
<h4><svg class="icon icon-sm icon-primary" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-inbox"></use></svg> 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>
@@ -106,7 +113,7 @@
</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>
<h4><svg class="icon icon-sm icon-primary" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-exchange-circle"></use></svg> 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>
@@ -116,39 +123,45 @@
</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>
<h4><svg class="icon icon-sm icon-primary" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-horn"></use></svg> 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>
<h4><svg class="icon icon-sm icon-primary" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-password-visible"></use></svg> 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>
<h4><svg class="icon icon-sm icon-primary" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-tool"></use></svg> Manutenzione Preventiva</h4>
<p>Script automatici prevengono il degrado:</p>
<table>
<tr><th>Operazione</th><th>Frequenza</th><th>Funzione</th></tr>
<caption class="sr-only">Operazioni di manutenzione preventiva</caption>
<thead><tr><th scope="col">Operazione</th><th scope="col">Frequenza</th><th scope="col">Funzione</th></tr></thead>
<tbody>
<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>
</tbody>
</table>
</div></div>
<h2><i class="fas fa-shield-halved"></i> 3. Protezione da Abuso</h2>
<h2><svg class="icon icon-primary" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-lock"></use></svg> 3. Protezione da Abuso</h2>
<h3>Rate Limiting</h3>
<table>
<tr><th>Endpoint</th><th>Limite</th><th>Finestra</th><th>Tipo</th></tr>
<caption class="sr-only">Limiti di rate limiting per endpoint</caption>
<thead><tr><th scope="col">Endpoint</th><th scope="col">Limite</th><th scope="col">Finestra</th><th scope="col">Tipo</th></tr></thead>
<tbody>
<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>
</tbody>
</table>
<h2><i class="fas fa-wrench"></i> 4. Modalita Manutenzione</h2>
<h2><svg class="icon icon-primary" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-tool"></use></svg> 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>
@@ -158,13 +171,15 @@
<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.
<svg class="icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-warning-circle"></use></svg>
<div><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>
</div>
<h2><i class="fas fa-exclamation-triangle"></i> 5. Matrice Rischi e Mitigazione</h2>
<h2><svg class="icon icon-primary" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-warning"></use></svg> 5. Matrice Rischi e Mitigazione</h2>
<table>
<tr><th>Scenario</th><th>Probabilita</th><th>Impatto</th><th>Mitigazione</th><th>Recovery</th></tr>
<caption class="sr-only">Matrice rischi, probabilita, impatto, mitigazione e recovery</caption>
<thead><tr><th scope="col">Scenario</th><th scope="col">Probabilita</th><th scope="col">Impatto</th><th scope="col">Mitigazione</th><th scope="col">Recovery</th></tr></thead>
<tbody>
<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>
@@ -172,19 +187,23 @@
<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>
</tbody>
</table>
<h2><i class="fas fa-handshake"></i> 6. Service Level Agreement</h2>
<h2><svg class="icon icon-primary" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-card"></use></svg> 6. Service Level Agreement</h2>
<table>
<tr><th>Metrica</th><th>Target</th><th>Misurazione</th></tr>
<caption class="sr-only">Metriche di Service Level Agreement</caption>
<thead><tr><th scope="col">Metrica</th><th scope="col">Target</th><th scope="col">Misurazione</th></tr></thead>
<tbody>
<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>
</tbody>
</table>
<h2><i class="fas fa-clipboard-check"></i> 7. Checklist Operativa</h2>
<h2><svg class="icon icon-primary" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-check-circle"></use></svg> 7. Checklist Operativa</h2>
<h3>Verifica automatica (quotidiana)</h3>
<ul>
<li>Tutti i container/processi in stato healthy/online</li>
@@ -199,11 +218,17 @@
<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>
</main>
<footer class="doc-footer">
<div>&copy; 2026 NIS2 Agile &mdash; <a href="https://nis2.agile.software">nis2.agile.software</a></div>
<div>Standard Continuita di Servizio v1.0 &bull; Aprile 2026 &bull; Agile Technology SRL</div>
</footer>
</div>
</div>
<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>
</body>
</html>
+43 -29
View File
@@ -4,6 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Impostazioni - NIS2 Agile</title>
<!-- 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 (tabelle/modali/form) -> 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>
.settings-tabs {
@@ -199,7 +203,7 @@
<main class="main-content">
<header class="content-header">
<h2 data-i18n="settings.title">Impostazioni</h2>
<h1 class="h2" data-i18n="settings.title">Impostazioni</h1>
<div class="content-header-actions">
<span class="text-muted" id="header-org-name"></span>
</div>
@@ -245,11 +249,11 @@
<form id="org-form" onsubmit="saveOrganization(event)">
<div class="form-row">
<div class="form-group">
<label class="form-label">Nome Organizzazione <span class="required">*</span></label>
<label for="org-name" class="form-label">Nome Organizzazione <span class="required">*</span></label>
<input type="text" class="form-input" id="org-name" required>
</div>
<div class="form-group">
<label class="form-label">Settore NIS2 <span class="required">*</span></label>
<label for="org-sector" class="form-label">Settore NIS2 <span class="required">*</span></label>
<select class="form-select" id="org-sector" required>
<option value="">Seleziona settore...</option>
<option value="energy">Energia</option>
@@ -275,55 +279,55 @@
<div class="form-row">
<div class="form-group">
<label class="form-label">Partita IVA</label>
<label for="org-vat" class="form-label">Partita IVA</label>
<input type="text" class="form-input" id="org-vat" placeholder="IT12345678901">
</div>
<div class="form-group">
<label class="form-label">Codice Fiscale</label>
<label for="org-fiscal" class="form-label">Codice Fiscale</label>
<input type="text" class="form-input" id="org-fiscal">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Numero Dipendenti</label>
<label for="org-employees" class="form-label">Numero Dipendenti</label>
<input type="number" class="form-input" id="org-employees" min="0" placeholder="0">
</div>
<div class="form-group">
<label class="form-label">Fatturato Annuo (EUR)</label>
<label for="org-turnover" class="form-label">Fatturato Annuo (EUR)</label>
<input type="number" class="form-input" id="org-turnover" min="0" step="1000" placeholder="0">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Paese</label>
<label for="org-country" class="form-label">Paese</label>
<input type="text" class="form-input" id="org-country" placeholder="IT">
</div>
<div class="form-group">
<label class="form-label">Citta'</label>
<label for="org-city" class="form-label">Citta'</label>
<input type="text" class="form-input" id="org-city">
</div>
</div>
<div class="form-group">
<label class="form-label">Indirizzo</label>
<label for="org-address" class="form-label">Indirizzo</label>
<input type="text" class="form-input" id="org-address">
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Sito Web</label>
<label for="org-website" class="form-label">Sito Web</label>
<input type="url" class="form-input" id="org-website" placeholder="https://...">
</div>
<div class="form-group">
<label class="form-label">Email di Contatto</label>
<label for="org-email" class="form-label">Email di Contatto</label>
<input type="email" class="form-input" id="org-email">
</div>
</div>
<div class="form-group">
<label class="form-label">Telefono di Contatto</label>
<label for="org-phone" class="form-label">Telefono di Contatto</label>
<input type="tel" class="form-input" id="org-phone" style="max-width:320px;">
</div>
@@ -349,24 +353,24 @@
<form id="profile-form" onsubmit="saveProfile(event)">
<div class="form-group">
<label class="form-label">Email</label>
<label for="profile-email" class="form-label">Email</label>
<input type="email" class="form-input" id="profile-email" disabled style="background:var(--gray-50); max-width:400px;">
<span class="form-help">L'email non puo' essere modificata.</span>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Nome Completo <span class="required">*</span></label>
<label for="profile-name" class="form-label">Nome Completo <span class="required">*</span></label>
<input type="text" class="form-input" id="profile-name" required>
</div>
<div class="form-group">
<label class="form-label">Telefono</label>
<label for="profile-phone" class="form-label">Telefono</label>
<input type="tel" class="form-input" id="profile-phone">
</div>
</div>
<div class="form-group" style="max-width:320px;">
<label class="form-label">Lingua Preferita</label>
<label for="profile-language" class="form-label">Lingua Preferita</label>
<select class="form-select" id="profile-language">
<option value="it">Italiano</option>
<option value="en">English</option>
@@ -391,12 +395,12 @@
<form id="password-form" onsubmit="changePassword(event)">
<div class="form-group" style="max-width:400px;">
<label class="form-label">Password Attuale <span class="required">*</span></label>
<label for="pw-current" class="form-label">Password Attuale <span class="required">*</span></label>
<input type="password" class="form-input" id="pw-current" required autocomplete="current-password">
</div>
<div class="form-group" style="max-width:400px;">
<label class="form-label">Nuova Password <span class="required">*</span></label>
<label for="pw-new" class="form-label">Nuova Password <span class="required">*</span></label>
<input type="password" class="form-input" id="pw-new" required autocomplete="new-password" oninput="checkPasswordStrength(this.value)">
<div class="strength-bar" id="pw-strength-bar">
<div class="strength-segment" id="pw-seg-1"></div>
@@ -408,7 +412,7 @@
</div>
<div class="form-group" style="max-width:400px;">
<label class="form-label">Conferma Nuova Password <span class="required">*</span></label>
<label for="pw-confirm" class="form-label">Conferma Nuova Password <span class="required">*</span></label>
<input type="password" class="form-input" id="pw-confirm" required autocomplete="new-password">
</div>
@@ -465,14 +469,14 @@
</div>
<div class="form-group">
<label style="display:flex; gap:10px; align-items:center; font-size:.9rem;">
<label for="pref-notif-email" style="display:flex; gap:10px; align-items:center; font-size:.9rem;">
<input type="checkbox" id="pref-notif-email">
<span>Ricevi notifiche via email (incidenti, deadline, training)</span>
</label>
</div>
<div class="form-group">
<label style="display:flex; gap:10px; align-items:center; font-size:.9rem;">
<label for="pref-notif-inapp" style="display:flex; gap:10px; align-items:center; font-size:.9rem;">
<input type="checkbox" id="pref-notif-inapp">
<span>Mostra notifiche in-app (badge sidebar)</span>
</label>
@@ -657,6 +661,16 @@
<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 src="/js/help.js"></script>
<script>
@@ -936,7 +950,7 @@
// Voluntary toggle (only visible for not_applicable entities)
const voluntaryToggle = type === 'not_applicable' ? `
<div style="margin-top:16px; padding:16px; background:var(--gray-50); border-radius:var(--border-radius); border:1px solid var(--gray-200);">
<label style="display:flex; align-items:center; gap:10px; cursor:pointer;">
<label for="org-voluntary" style="display:flex; align-items:center; gap:10px; cursor:pointer;">
<input type="checkbox" id="org-voluntary" ${isVoluntary ? 'checked' : ''}
onchange="toggleVoluntaryCompliance(this.checked)"
style="accent-color:var(--primary); width:18px; height:18px; flex-shrink:0;">
@@ -1234,11 +1248,11 @@
showModal('Invita Membro', `
<p style="margin-bottom:16px; font-size:0.875rem; color:var(--gray-600);">Aggiungi un membro esistente alla tua organizzazione. L'utente deve gia' essere registrato sulla piattaforma.</p>
<div class="form-group">
<label class="form-label">Email del Membro <span class="required">*</span></label>
<label for="invite-email" class="form-label">Email del Membro <span class="required">*</span></label>
<input type="email" class="form-input" id="invite-email" placeholder="utente@esempio.it" required>
</div>
<div class="form-group">
<label class="form-label">Ruolo <span class="required">*</span></label>
<label for="invite-role" class="form-label">Ruolo <span class="required">*</span></label>
<select class="form-select" id="invite-role">
<option value="employee">Dipendente</option>
<option value="compliance_manager">Compliance Manager</option>
@@ -1368,7 +1382,7 @@
function showCreateApiKeyModal() {
showModal('Crea API Key', `
<div class="form-group">
<label class="form-label">Nome *</label>
<label for="apikey-name" class="form-label">Nome *</label>
<input type="text" id="apikey-name" class="form-control" placeholder="Es. SIEM Integration, Dashboard ESG..." required>
</div>
<div class="form-group">
@@ -1383,7 +1397,7 @@
</div>
</div>
<div class="form-group">
<label class="form-label">Scadenza (opzionale)</label>
<label for="apikey-expires" class="form-label">Scadenza (opzionale)</label>
<input type="date" id="apikey-expires" class="form-control">
</div>
`, '<button class="btn btn-primary" onclick="createApiKey()">Crea API Key</button>');
@@ -1485,11 +1499,11 @@
).join('');
showModal('Crea Webhook', `
<div class="form-group">
<label class="form-label">Nome *</label>
<label for="wh-name" class="form-label">Nome *</label>
<input type="text" id="wh-name" class="form-control" placeholder="Es. SIEM Integration, 231 Agile Notify...">
</div>
<div class="form-group">
<label class="form-label">URL Endpoint * (https://...)</label>
<label for="wh-url" class="form-label">URL Endpoint * (https://...)</label>
<input type="url" id="wh-url" class="form-control" placeholder="https://your-siem.example.com/webhooks/nis2">
</div>
<div class="form-group">
+36 -22
View File
@@ -4,6 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Supply Chain - NIS2 Agile</title>
<!-- 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 (tabelle/modali/form) -> 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>
/* ── Supply Chain Page Styles ─────────────────────────── */
@@ -368,11 +372,11 @@
<main class="main-content">
<header class="content-header">
<h2 data-i18n="supply_chain.title">Sicurezza Supply Chain</h2>
<h1 class="h2" data-i18n="supply_chain.title">Sicurezza Supply Chain</h1>
<div class="content-header-actions">
<button class="btn btn-secondary" onclick="openCategoriesModal()"><i class="fas fa-tags"></i> <span data-i18n="sp.categories_btn">Categorie</span></button>
<button class="btn btn-secondary" onclick="openTemplatesModal()"><i class="fas fa-clipboard-list"></i> <span data-i18n="sp.templates_btn">Template</span></button>
<button class="btn btn-secondary" onclick="openImportModal()"><i class="fas fa-file-import"></i> <span data-i18n="sp.import_btn">Importa</span></button>
<button class="btn btn-secondary" onclick="openCategoriesModal()"><svg class="icon icon-sm" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-bookmark"></use></svg> <span data-i18n="sp.categories_btn">Categorie</span></button>
<button class="btn btn-secondary" onclick="openTemplatesModal()"><svg class="icon icon-sm" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-files"></use></svg> <span data-i18n="sp.templates_btn">Template</span></button>
<button class="btn btn-secondary" onclick="openImportModal()"><svg class="icon icon-sm" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-upload"></use></svg> <span data-i18n="sp.import_btn">Importa</span></button>
<button class="btn btn-primary" onclick="openCreateModal()"><span data-i18n="sp.new_supplier">+ Nuovo Fornitore</span></button>
</div>
</header>
@@ -428,14 +432,14 @@
<!-- Filters + Table -->
<div id="suppliers-list-section">
<div class="filters-bar">
<select class="form-select" id="filter-criticality" onchange="applyFilters()">
<select class="form-select" id="filter-criticality" onchange="applyFilters()" aria-label="Filtra per criticità">
<option value="">Tutte le Criticita'</option>
<option value="critical">Critico</option>
<option value="high">Alto</option>
<option value="medium">Medio</option>
<option value="low">Basso</option>
</select>
<select class="form-select" id="filter-status" onchange="applyFilters()">
<select class="form-select" id="filter-status" onchange="applyFilters()" aria-label="Filtra per stato">
<option value="">Tutti gli Stati</option>
<option value="active">Attivo</option>
<option value="under_review">In Revisione</option>
@@ -462,6 +466,16 @@
<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 src="/js/help.js"></script>
<script>
@@ -920,31 +934,31 @@
<form id="supplier-form" onsubmit="return false;">
<div class="form-row" style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div class="form-group">
<label class="form-label">Nome Fornitore <span class="required">*</span></label>
<label for="form-name" class="form-label">Nome Fornitore <span class="required">*</span></label>
<input type="text" class="form-input" id="form-name" value="${isEdit ? escapeHtml(supplier.name) : ''}" required placeholder="Nome dell'azienda">
</div>
<div class="form-group">
<label class="form-label">Partita IVA</label>
<label for="form-vat" class="form-label">Partita IVA</label>
<input type="text" class="form-input" id="form-vat" value="${isEdit && supplier.vat_number ? escapeHtml(supplier.vat_number) : ''}" placeholder="IT01234567890">
</div>
</div>
<div class="form-row" style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div class="form-group">
<label class="form-label">Referente</label>
<label for="form-contact-name" class="form-label">Referente</label>
<input type="text" class="form-input" id="form-contact-name" value="${isEdit && supplier.contact_name ? escapeHtml(supplier.contact_name) : ''}" placeholder="Nome e cognome">
</div>
<div class="form-group">
<label class="form-label">Email Contatto</label>
<label for="form-contact-email" class="form-label">Email Contatto</label>
<input type="email" class="form-input" id="form-contact-email" value="${isEdit && supplier.contact_email ? escapeHtml(supplier.contact_email) : ''}" placeholder="email@azienda.it">
</div>
</div>
<div class="form-row" style="display:grid;grid-template-columns:2fr 1fr;gap:16px;">
<div class="form-group">
<label class="form-label">Tipo Servizio <span class="required">*</span></label>
<label for="form-service-type" class="form-label">Tipo Servizio <span class="required">*</span></label>
<input type="text" class="form-input" id="form-service-type" value="${isEdit && supplier.service_type ? escapeHtml(supplier.service_type) : ''}" required placeholder="es. Cloud Hosting, Sviluppo SW, Consulenza">
</div>
<div class="form-group">
<label class="form-label">Criticita'</label>
<label for="form-criticality" class="form-label">Criticita'</label>
<select class="form-select" id="form-criticality">
${criticalityOptions}
</select>
@@ -958,21 +972,21 @@
<small style="color:var(--gray-500);font-size:0.75rem;">Classifica il fornitore (es. Cloud Provider). Gestisci le categorie dalla tab "Categorie".</small>
</div>
<div class="form-group">
<label class="form-label">Descrizione Servizio</label>
<label for="form-service-desc" class="form-label">Descrizione Servizio</label>
<textarea class="form-textarea" id="form-service-desc" rows="3" placeholder="Descrivi il servizio fornito...">${isEdit && supplier.service_description ? escapeHtml(supplier.service_description) : ''}</textarea>
</div>
<div class="form-row" style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div class="form-group">
<label class="form-label">Inizio Contratto</label>
<label for="form-contract-start" class="form-label">Inizio Contratto</label>
<input type="date" class="form-input" id="form-contract-start" value="${isEdit && supplier.contract_start_date ? supplier.contract_start_date.substring(0, 10) : ''}">
</div>
<div class="form-group">
<label class="form-label">Scadenza Contratto</label>
<label for="form-contract-expiry" class="form-label">Scadenza Contratto</label>
<input type="date" class="form-input" id="form-contract-expiry" value="${isEdit && supplier.contract_expiry_date ? supplier.contract_expiry_date.substring(0, 10) : ''}">
</div>
</div>
<div class="form-group">
<label class="form-label">Note</label>
<label for="form-notes" class="form-label">Note</label>
<textarea class="form-textarea" id="form-notes" rows="3" placeholder="Note aggiuntive...">${isEdit && supplier.notes ? escapeHtml(supplier.notes) : ''}</textarea>
</div>
</form>
@@ -1198,7 +1212,7 @@
ov.innerHTML = `
<div style="background:#fff;border-radius:10px;max-width:640px;width:100%;max-height:90vh;overflow:auto;">
<div style="display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #e2e8f0;">
<h3 style="margin:0;"><i class="fas fa-file-import"></i> Importa fornitori</h3>
<h3 style="margin:0;"><svg class="icon icon-sm" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-upload"></use></svg> Importa fornitori</h3>
<button onclick="document.getElementById('importModalDyn').remove()" style="background:none;border:none;font-size:22px;cursor:pointer;line-height:1;">&times;</button>
</div>
<div style="padding:20px;">
@@ -1208,14 +1222,14 @@
<code>service_type</code>, <code>criticality</code> (low/medium/high/critical),
<code>category_slug</code>, <code>external_ref</code>. I record con stesso <code>external_ref</code> vengono aggiornati.
</p>
<input type="file" id="importFile" accept=".csv,text/csv" class="form-input" style="margin-bottom:10px;">
<textarea id="importCsv" class="form-input" rows="8" style="width:100%;font-family:monospace;font-size:13px;"
<input type="file" id="importFile" accept=".csv,text/csv" class="form-input" style="margin-bottom:10px;" aria-label="Seleziona file CSV fornitori da importare">
<textarea id="importCsv" class="form-input" rows="8" style="width:100%;font-family:monospace;font-size:13px;" aria-label="Dati CSV fornitori"
placeholder="name,vat_number,contact_email,criticality,category_slug&#10;ACME Srl,IT01234567890,security@acme.it,high,cloud_provider"></textarea>
<div id="importResult" style="margin-top:10px;font-size:14px;"></div>
</div>
<div style="display:flex;justify-content:flex-end;gap:10px;padding:16px 20px;border-top:1px solid #e2e8f0;">
<button class="btn btn-secondary" onclick="document.getElementById('importModalDyn').remove()">Annulla</button>
<button class="btn btn-primary" id="importRunBtn" onclick="runSupplierImport()"><i class="fas fa-upload"></i> Importa</button>
<button class="btn btn-primary" id="importRunBtn" onclick="runSupplierImport()"><svg class="icon icon-sm icon-white" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-upload"></use></svg> Importa</button>
</div>
</div>`;
document.body.appendChild(ov);
@@ -1272,7 +1286,7 @@
resEl.innerHTML = '<span style="color:#dc2626;">Troppi record (max 1000).</span>';
return;
}
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Importo...';
btn.disabled = true; btn.innerHTML = '<span class="progress-spinner progress-spinner-active size-sm progress-spinner-inverse" role="status"><span class="visually-hidden">Caricamento…</span></span> Importo...';
try {
const res = await api.importSuppliers(rows);
if (res.success) {
@@ -1293,7 +1307,7 @@
} catch (e) {
resEl.innerHTML = `<span style="color:#dc2626;">Errore: ${(e && e.message) || e}</span>`;
} finally {
btn.disabled = false; btn.innerHTML = '<i class="fas fa-upload"></i> Importa';
btn.disabled = false; btn.innerHTML = '<svg class="icon icon-sm icon-white" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-upload"></use></svg> Importa';
}
}
+15 -1
View File
@@ -4,6 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Formazione - NIS2 Agile</title>
<!-- 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 (tabelle/modali/form) -> 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>
/* ── Tab Navigation ─────────────────────────────────────── */
@@ -224,7 +228,7 @@
<main class="main-content">
<header class="content-header">
<h2 data-i18n="training.title">Formazione e Awareness</h2>
<h1 class="h2" data-i18n="training.title">Formazione e Awareness</h1>
<div class="content-header-actions">
<span class="text-muted" style="font-size:0.8rem; margin-right:8px;">Art. 20 NIS2</span>
<button class="btn btn-primary" onclick="showCreateCourseModal()">+ Nuovo Corso</button>
@@ -277,6 +281,16 @@
<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 src="/js/help.js"></script>
<script>
+41 -19
View File
@@ -3,7 +3,12 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Segnalazioni - NIS2 Agile</title>
<title>Segnalazioni Sicurezza - NIS2 Agile</title>
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di
style.css cosi' il design system esistente resta autorevole per i widget
renderizzati via JS (tabelle/modali/timeline) -> parita' funzionale assoluta.
BI veste la chrome (sidebar + font istituzionale). -->
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.priority-badge { display: inline-flex; align-items: center; padding: 2px 10px; border-radius: 12px; font-size: 0.7rem; font-weight: 700; }
@@ -22,11 +27,13 @@
.art32-banner h3 { font-size: 0.9375rem; font-weight: 700; color: var(--gray-800); margin-bottom: 6px; }
.art32-banner p { font-size: 0.8125rem; color: var(--gray-600); }
.track-form { background: var(--gray-50); border: 1px solid var(--gray-200); border-radius: var(--border-radius); padding: 20px; margin-bottom: 24px; }
/* Icone sprite BI dimensionate inline per i bottoni azione */
.icon-inline { width: 16px; height: 16px; fill: currentColor; vertical-align: text-bottom; }
</style>
</head>
<body>
<div class="app-layout">
<div id="sidebar-container"></div>
<aside class="sidebar" id="sidebar"></aside>
<main class="main-content">
<div class="page-header">
<div class="page-header-content">
@@ -35,11 +42,11 @@
</div>
<div class="page-header-actions">
<button class="btn btn-secondary" onclick="switchView('track')">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd"/></svg>
<svg class="icon-inline" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-search"></use></svg>
Traccia Segnalazione Anonima
</button>
<button class="btn btn-primary" onclick="switchView('submit')">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/></svg>
<svg class="icon-inline" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-plus"></use></svg>
Nuova Segnalazione
</button>
</div>
@@ -62,7 +69,8 @@
<div class="card mb-16">
<div class="card-body" style="padding:16px 20px;">
<div style="display:flex; gap:12px; flex-wrap:wrap; align-items:center;">
<select id="filter-status" class="form-control" style="width:auto;" onchange="loadReports()">
<label for="filter-status" class="sr-only">Filtra per stato</label>
<select id="filter-status" class="form-control" style="width:auto;" onchange="loadReports()" aria-label="Filtra per stato">
<option value="">Tutti gli stati</option>
<option value="received">Ricevute</option>
<option value="under_review">In revisione</option>
@@ -70,14 +78,16 @@
<option value="resolved">Risolte</option>
<option value="closed">Chiuse</option>
</select>
<select id="filter-priority" class="form-control" style="width:auto;" onchange="loadReports()">
<label for="filter-priority" class="sr-only">Filtra per priorità</label>
<select id="filter-priority" class="form-control" style="width:auto;" onchange="loadReports()" aria-label="Filtra per priorità">
<option value="">Tutte le priorità</option>
<option value="critical">Critica</option>
<option value="high">Alta</option>
<option value="medium">Media</option>
<option value="low">Bassa</option>
</select>
<select id="filter-category" class="form-control" style="width:auto;" onchange="loadReports()">
<label for="filter-category" class="sr-only">Filtra per categoria</label>
<select id="filter-category" class="form-control" style="width:auto;" onchange="loadReports()" aria-label="Filtra per categoria">
<option value="">Tutte le categorie</option>
<option value="security_incident">Incidente sicurezza</option>
<option value="data_breach">Data breach</option>
@@ -102,7 +112,7 @@
</div>
<div class="card-body">
<div style="margin-bottom:20px; padding:12px 16px; background:rgba(16,185,129,0.08); border:1px solid rgba(16,185,129,0.2); border-radius:var(--border-radius); display:flex; align-items:center; gap:10px;">
<svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18" style="color:var(--success); flex-shrink:0;"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/></svg>
<svg class="icon-inline" style="width:18px; height:18px; color:var(--success); flex-shrink:0;" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-lock"></use></svg>
<label style="display:flex; align-items:center; gap:8px; font-size:0.875rem; cursor:pointer;">
<input type="checkbox" id="is-anonymous" checked style="accent-color:var(--success);">
<span><strong>Segnalazione anonima</strong> — La tua identità non verrà registrata</span>
@@ -111,7 +121,7 @@
<form id="submit-form" onsubmit="submitReport(event)">
<div class="form-group">
<label class="form-label">Categoria *</label>
<label class="form-label" for="rep-category">Categoria *</label>
<select id="rep-category" class="form-control" required>
<option value="">Seleziona categoria...</option>
<option value="security_incident">Incidente di sicurezza</option>
@@ -125,15 +135,15 @@
</select>
</div>
<div class="form-group">
<label class="form-label">Titolo *</label>
<label class="form-label" for="rep-title">Titolo *</label>
<input type="text" id="rep-title" class="form-control" placeholder="Breve descrizione dell'anomalia..." required>
</div>
<div class="form-group">
<label class="form-label">Descrizione dettagliata *</label>
<label class="form-label" for="rep-description">Descrizione dettagliata *</label>
<textarea id="rep-description" class="form-control" rows="6" placeholder="Descrivi cosa hai osservato, quando è successo, chi è coinvolto (se noto), e qualsiasi altra informazione utile..." required></textarea>
</div>
<div class="form-group">
<label class="form-label">Priorità stimata</label>
<label class="form-label" for="rep-priority">Priorità stimata</label>
<select id="rep-priority" class="form-control">
<option value="medium">Media</option>
<option value="high">Alta</option>
@@ -142,11 +152,11 @@
</select>
</div>
<div class="form-group">
<label class="form-label">Articolo NIS2 violato (opzionale)</label>
<label class="form-label" for="rep-article">Articolo NIS2 violato (opzionale)</label>
<input type="text" id="rep-article" class="form-control" placeholder="Es. Art.21, Art.23...">
</div>
<div class="form-group" id="contact-email-group">
<label class="form-label">Email per follow-up (opzionale)</label>
<label class="form-label" for="rep-email">Email per follow-up (opzionale)</label>
<input type="email" id="rep-email" class="form-control" placeholder="Email solo per ricevere aggiornamenti">
<p style="font-size:0.75rem; color:var(--gray-500); margin-top:4px;">Lascia vuoto per mantenere l'anonimato completo. Oppure fornisci un'email temporanea.</p>
</div>
@@ -168,7 +178,7 @@
</div>
<div class="card-body">
<div class="form-group">
<label class="form-label">Token Segnalazione</label>
<label class="form-label" for="track-token">Token Segnalazione</label>
<input type="text" id="track-token" class="form-control" placeholder="Inserisci il token...">
</div>
<div style="display:flex; gap:12px;">
@@ -186,6 +196,16 @@
<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 src="/js/help.js"></script>
<script>
@@ -269,7 +289,7 @@
if (!reports.length) {
container.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 20 20" fill="currentColor" width="40" height="40" style="color:var(--gray-300)"><path fill-rule="evenodd" d="M10 1.944A11.954 11.954 0 012.166 5C2.056 5.649 2 6.319 2 7c0 5.225 3.34 9.67 8 11.317C14.66 16.67 18 12.225 18 7c0-.682-.057-1.35-.166-2.001A11.954 11.954 0 0110 1.944zM11 14a1 1 0 11-2 0 1 1 0 012 0zm0-7a1 1 0 10-2 0v3a1 1 0 102 0V7z" clip-rule="evenodd"/></svg>
<svg class="icon-inline" style="width:40px; height:40px; color:var(--gray-300);" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-warning-circle"></use></svg>
<h4>Nessuna segnalazione trovata</h4>
<p>Il canale di segnalazione è attivo. Le segnalazioni appariranno qui.</p>
</div>`;
@@ -277,7 +297,7 @@
}
let html = `<div class="table-container"><table>
<thead><tr><th>Codice</th><th>Categoria</th><th>Titolo</th><th>Priorità</th><th>Stato</th><th>Anonima</th><th>Assegnata</th><th>Data</th><th></th></tr></thead><tbody>`;
<thead><tr><th scope="col">Codice</th><th scope="col">Categoria</th><th scope="col">Titolo</th><th scope="col">Priorità</th><th scope="col">Stato</th><th scope="col">Anonima</th><th scope="col">Assegnata</th><th scope="col">Data</th><th scope="col"></th></tr></thead><tbody>`;
reports.forEach(r => {
html += `<tr>
@@ -286,7 +306,7 @@
<td><strong style="font-size:0.875rem;">${escapeHtml(r.title)}</strong></td>
<td><span class="priority-badge priority-${r.priority}">${escapeHtml(priorityLabels[r.priority] || r.priority)}</span></td>
<td><span class="status-badge status-${r.status}">${escapeHtml(statusLabels[r.status] || r.status)}</span></td>
<td>${r.is_anonymous ? '<span class="anonymous-shield"><svg viewBox="0 0 20 20" fill="currentColor" width="12" height="12"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/></svg>Anonima</span>' : '<span style="color:var(--gray-400); font-size:0.75rem;">Firmata</span>'}</td>
<td>${r.is_anonymous ? '<span class="anonymous-shield"><svg class="icon-inline" style="width:12px;height:12px;" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-lock"></use></svg>Anonima</span>' : '<span style="color:var(--gray-400); font-size:0.75rem;">Firmata</span>'}</td>
<td>${r.assigned_to_name ? escapeHtml(r.assigned_to_name) : '<span style="color:var(--gray-400);">—</span>'}</td>
<td style="font-size:0.8rem;">${formatDate(r.created_at)}</td>
<td><button class="btn btn-sm btn-secondary" onclick="viewReport(${r.id})">Gestisci</button></td>
@@ -315,7 +335,7 @@
<div style="display:flex; gap:10px; flex-wrap:wrap; margin-bottom:16px;">
<span class="priority-badge priority-${r.priority}">${escapeHtml(priorityLabels[r.priority] || r.priority)}</span>
<span class="status-badge status-${r.status}">${escapeHtml(statusLabels[r.status] || r.status)}</span>
${r.is_anonymous ? '<span class="anonymous-shield"><svg viewBox="0 0 20 20" fill="currentColor" width="12" height="12"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/></svg>Anonima</span>' : ''}
${r.is_anonymous ? '<span class="anonymous-shield"><svg class="icon-inline" style="width:12px;height:12px;" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-lock"></use></svg>Anonima</span>' : ''}
</div>
<div style="padding:12px 16px; background:var(--gray-50); border-radius:var(--border-radius); margin-bottom:16px; font-size:0.875rem; line-height:1.6;">
${escapeHtml(r.description)}
@@ -329,12 +349,14 @@
<h4 style="font-size:0.8125rem; font-weight:700; margin-bottom:8px;">Timeline</h4>
<div>${timelineHtml || '<p style="color:var(--gray-400); font-size:0.8rem;">Nessun evento in timeline.</p>'}</div>
<div style="margin-top:20px; display:flex; flex-direction:column; gap:10px;">
<label for="modal-status" class="sr-only">Cambia stato</label>
<select id="modal-status" class="form-control">
<option value="">Cambia stato...</option>
${['under_review','investigating','resolved','closed','rejected'].map(s =>
`<option value="${s}" ${r.status === s ? 'selected' : ''}>${escapeHtml(statusLabels[s])}</option>`
).join('')}
</select>
<label for="modal-notes" class="sr-only">Note di risoluzione</label>
<textarea id="modal-notes" class="form-control" rows="3" placeholder="Note di risoluzione...">${escapeHtml(r.resolution_notes || '')}</textarea>
</div>
`, `<button class="btn btn-primary" onclick="updateReport(${id})">Salva</button>