Completa C5 (Epic C). Gli stakeholder rispondono in self-service tramite magic-link
(token SHA-256 per destinatario, NESSUN account/JWT); il compliance manager vede gli
esiti, i commenti e gli allegati nel dettaglio dell'attività.
- StakeholderPortalController (non-JWT, token-only): access / respond (questionario) /
acknowledge (firma di avvenuta lettura) / comment / attachment. Submit one-shot (409),
validazione risposte obbligatorie, anti-IDOR (un token = un solo destinatario),
auto-completamento attività quando tutti hanno risposto/firmato.
- StakeholderActivityController: feedback (risposte per destinatario), comments
(GET/POST), attachments (upload interno + lista; riuso evidence_files entity_type=
'stk_activity', file sotto public/uploads/stk_activity/{org}/).
- mig.053: stk_activity_responses (answers JSON / acknowledged_at), stk_activity_comments
(interni/esterni). Estende il seeder idempotente.
- Frontend: stk-portal.html (pagina pubblica dependency-free: questionario per tipo di
domanda o testo+firma, commento, upload); dettaglio attività in stakeholder-activities.html
con esiti, thread commenti e allegati.
Email disattivate (kill-switch) → i magic-link si condividono manualmente. Smoke prod OK
(access no-auth, respond+required+409, acknowledge+WRONG_TYPE, comment esterno/interno,
bad-token 404, feedback interno, auto-complete; cleanup org 151 pulita). Additivo. v1.21.0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
287 lines
17 KiB
HTML
287 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="it">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
<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?v=20260613b">
|
|
<style>
|
|
.impact-badge { display: inline-flex; align-items: center; padding: 2px 10px; border-radius: 12px; font-size: 0.7rem; font-weight: 700; }
|
|
.impact-critical { background: var(--danger-bg); color: var(--danger); }
|
|
.impact-high { background: #fff7ed; color: #c2410c; }
|
|
.impact-medium { background: var(--warning-bg); color: #a16207; }
|
|
.impact-low { background: var(--gray-100); color: var(--gray-600); }
|
|
.impact-informational { background: rgba(6,182,212,0.1); color: #0891b2; }
|
|
.ack-badge { display: inline-flex; align-items: center; gap: 5px; padding: 2px 10px; border-radius: 12px; font-size: 0.7rem; font-weight: 600; }
|
|
.ack-done { background: var(--success-bg); color: var(--success); }
|
|
.ack-pending { background: var(--warning-bg); color: #a16207; }
|
|
.source-chip { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 4px; font-size: 0.7rem; font-weight: 700; background: var(--gray-100); color: var(--gray-600); font-family: monospace; }
|
|
.update-card { border: 1px solid var(--gray-200); border-radius: var(--border-radius-lg); padding: 20px; margin-bottom: 16px; transition: all var(--transition-fast); }
|
|
.update-card:hover { border-color: var(--primary); box-shadow: 0 2px 8px rgba(6,182,212,0.1); }
|
|
.update-card.action-required { border-left: 3px solid var(--warning); }
|
|
.update-card.acked { opacity: 0.7; }
|
|
.ack-progress { height: 6px; background: var(--gray-200); border-radius: 3px; overflow: hidden; }
|
|
.ack-progress-bar { height: 100%; background: var(--primary); border-radius: 3px; transition: width 0.5s ease; }
|
|
</style>
|
|
<!-- PWA:start -->
|
|
<link rel="manifest" href="/manifest.webmanifest">
|
|
<meta name="theme-color" content="#0066CC">
|
|
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32.png">
|
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16.png">
|
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
|
<meta name="apple-mobile-web-app-title" content="NIS2 Agile">
|
|
<meta name="application-name" content="NIS2 Agile">
|
|
<script src="/js/pwa.js?v=20260614" defer></script>
|
|
<!-- PWA:end -->
|
|
</head>
|
|
<body>
|
|
<div class="app-layout">
|
|
<aside class="sidebar" id="sidebar"></aside>
|
|
<main class="main-content">
|
|
<div class="page-header">
|
|
<div class="page-header-content">
|
|
<h1 class="page-title">Aggiornamenti Normativi</h1>
|
|
<p class="page-subtitle">Feed NIS2 / ACN / DORA — Presa visione obbligatoria per audit compliance</p>
|
|
</div>
|
|
<div class="page-header-actions">
|
|
<button class="btn btn-secondary" onclick="loadPending()">
|
|
<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>
|
|
</div>
|
|
|
|
<!-- Stats -->
|
|
<div class="stats-grid mb-24" id="stats-grid">
|
|
<div class="stat-card"><div class="spinner"></div></div>
|
|
</div>
|
|
|
|
<!-- Ack Progress bar -->
|
|
<div class="card mb-24">
|
|
<div class="card-body" style="padding:16px 20px;">
|
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
|
|
<span style="font-size:0.875rem; font-weight:600; color:var(--gray-700);">Avanzamento presa visione</span>
|
|
<span id="ack-pct" style="font-size:0.875rem; font-weight:700; color:var(--primary);">—</span>
|
|
</div>
|
|
<div class="ack-progress">
|
|
<div class="ack-progress-bar" id="ack-bar" style="width:0%"></div>
|
|
</div>
|
|
<p style="font-size:0.75rem; color:var(--gray-500); margin-top:6px;">Documentare la presa visione degli aggiornamenti normativi è richiesto per la compliance NIS2 Art.21.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<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;">
|
|
<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>
|
|
<option value="acn_guideline">ACN Guideline</option>
|
|
<option value="dora">DORA</option>
|
|
<option value="enisa">ENISA</option>
|
|
<option value="iso27001">ISO 27001</option>
|
|
</select>
|
|
<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>
|
|
<option value="medium">Medio</option>
|
|
<option value="low">Basso</option>
|
|
<option value="informational">Informativo</option>
|
|
</select>
|
|
<label style="display:flex; align-items:center; gap:8px; font-size:0.875rem; cursor:pointer;">
|
|
<input type="checkbox" id="filter-action" onchange="loadUpdates()" style="accent-color:var(--primary);">
|
|
Solo azione richiesta
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="updates-container"><div class="spinner" style="margin:60px auto;"></div></div>
|
|
</main>
|
|
</div>
|
|
|
|
<script src="/js/api.js?v=20260628"></script>
|
|
<script src="/js/common.js?v=20260628"></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?v=20260628"></script>
|
|
<script src="/js/i18n.js?v=20260628"></script>
|
|
<script src="/js/help.js?v=20260628"></script>
|
|
<script>
|
|
if (!checkAuth()) throw new Error('Not authenticated');
|
|
loadSidebar();
|
|
I18n.init();
|
|
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
|
|
|
|
const sourceLabels = {
|
|
nis2_directive: 'NIS2 Directive', dlgs_138_2024: 'D.Lgs. 138/2024',
|
|
acn_guideline: 'ACN', dora: 'DORA', enisa: 'ENISA', iso27001: 'ISO 27001', other: 'Altro'
|
|
};
|
|
const impactLabels = {
|
|
critical: 'Critico', high: 'Alto', medium: 'Medio', low: 'Basso', informational: 'Informativo'
|
|
};
|
|
|
|
async function loadStats() {
|
|
try {
|
|
const result = await api.request('GET', '/normative/stats');
|
|
if (result.success) renderStats(result.data);
|
|
} catch (e) {}
|
|
}
|
|
|
|
function renderStats(d) {
|
|
document.getElementById('stats-grid').innerHTML = `
|
|
<div class="stat-card">
|
|
<div class="stat-label">Aggiornamenti Totali</div>
|
|
<div class="stat-value">${d.total_updates || 0}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Prese Visione</div>
|
|
<div class="stat-value" style="color:var(--success);">${d.acknowledged || 0}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Pendenti</div>
|
|
<div class="stat-value" style="color:var(--warning);">${d.pending || 0}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Compliance ACK</div>
|
|
<div class="stat-value" style="color:var(--primary);">${d.ack_rate || 0}%</div>
|
|
</div>`;
|
|
|
|
document.getElementById('ack-pct').textContent = (d.ack_rate || 0) + '%';
|
|
document.getElementById('ack-bar').style.width = (d.ack_rate || 0) + '%';
|
|
}
|
|
|
|
async function loadUpdates() {
|
|
const container = document.getElementById('updates-container');
|
|
container.innerHTML = '<div class="spinner" style="margin:60px auto;"></div>';
|
|
try {
|
|
const params = new URLSearchParams();
|
|
const source = document.getElementById('filter-source').value;
|
|
const impact = document.getElementById('filter-impact').value;
|
|
const action = document.getElementById('filter-action').checked;
|
|
if (source) params.append('source', source);
|
|
if (impact) params.append('impact', impact);
|
|
if (action) params.append('action_required', '1');
|
|
|
|
const result = await api.request('GET', '/normative/list?' + params);
|
|
if (result.success) renderUpdates(result.data.updates || []);
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state"><h4>Errore caricamento aggiornamenti</h4></div>';
|
|
}
|
|
}
|
|
|
|
async function loadPending() {
|
|
const container = document.getElementById('updates-container');
|
|
container.innerHTML = '<div class="spinner" style="margin:60px auto;"></div>';
|
|
try {
|
|
const result = await api.request('GET', '/normative/pending');
|
|
if (result.success) renderUpdates(result.data.pending || [], true);
|
|
} catch (e) { container.innerHTML = '<div class="empty-state"><h4>Errore.</h4></div>'; }
|
|
}
|
|
|
|
function renderUpdates(updates, pendingOnly) {
|
|
const container = document.getElementById('updates-container');
|
|
if (!updates.length) {
|
|
container.innerHTML = `<div class="empty-state">
|
|
<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;
|
|
}
|
|
|
|
let html = '';
|
|
updates.forEach(u => {
|
|
const acked = u.is_acknowledged || !!(u.acknowledged_at);
|
|
const actionClass = u.action_required ? 'action-required' : '';
|
|
const ackedClass = acked ? 'acked' : '';
|
|
const domains = (u.affected_domains || []).map(d =>
|
|
`<span class="source-chip">${escapeHtml(d)}</span>`
|
|
).join(' ');
|
|
|
|
html += `<div class="update-card ${actionClass} ${ackedClass}" id="update-${u.id}">
|
|
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:12px; flex-wrap:wrap;">
|
|
<div style="flex:1;">
|
|
<div style="display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:8px;">
|
|
<span class="source-chip">${escapeHtml(sourceLabels[u.source] || u.source)}</span>
|
|
<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 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>
|
|
<h3 style="font-size:1rem; font-weight:700; color:var(--gray-900); margin-bottom:6px;">${escapeHtml(u.title)}</h3>
|
|
${u.reference ? `<p style="font-size:0.75rem; color:var(--gray-500); margin-bottom:8px;"><code>${escapeHtml(u.reference)}</code></p>` : ''}
|
|
<p style="font-size:0.8125rem; color:var(--gray-600); line-height:1.6; margin-bottom:10px;">${escapeHtml(u.summary)}</p>
|
|
${domains ? `<div style="display:flex; gap:6px; flex-wrap:wrap;">${domains}</div>` : ''}
|
|
</div>
|
|
<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 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 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>
|
|
</div>
|
|
</div>`;
|
|
});
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
async function acknowledgeUpdate(id) {
|
|
showModal('Presa Visione', `
|
|
<p style="font-size:0.875rem; color:var(--gray-600); margin-bottom:16px;">
|
|
Documentare la presa visione dell'aggiornamento normativo. Opzionalmente aggiungere note sull'impatto per la tua organizzazione.
|
|
</p>
|
|
<div class="form-group">
|
|
<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>
|
|
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>`);
|
|
}
|
|
|
|
async function confirmAck(id) {
|
|
const notes = document.getElementById('ack-notes').value;
|
|
try {
|
|
const result = await api.request('POST', `/normative/${id}/ack`, { notes });
|
|
if (result.success) {
|
|
closeModal();
|
|
showNotification('Presa visione registrata e documentata.', 'success');
|
|
loadUpdates();
|
|
loadStats();
|
|
} else showNotification(result.message || 'Errore.', 'error');
|
|
} catch (e) { showNotification('Errore di connessione.', 'error'); }
|
|
}
|
|
|
|
// Init
|
|
loadStats();
|
|
loadUpdates();
|
|
</script>
|
|
</body>
|
|
</html>
|