[FEAT] Epic C / C4 — inventario a 2 voci + sottoclassi configurabili (mig.050)

Simon C4: nell'inventario DUE sole voci principali; le sottoclassi le crea l'utente.
Riconduzione concordata (no perdita di dettaglio): hardware/network/facility -> ID.AM-01;
software/service/data -> ID.AM-02; personnel -> Organigramma (0 asset personnel presenti).

- Config: cfg_inventory_voci (2: ID.AM-01 Hardware, ID.AM-02 Software/Servizi/Sistemi),
  inventory_subclassi (12 default di sistema org NULL + sottoclassi org-scoped).
- assets.voce_code + subclass_id (FK). 20 asset esistenti MIGRATI (5 ID.AM-01 / 15 ID.AM-02).
- AssetController: list con label voce/sottoclasse; create/update validati (voce in whitelist,
  sottoclasse appartenente a org/default+voce); GET/POST /assets/subclasses; import (bulkUpsert)
  deriva la voce dal tipo legacy. asset_type resta come campo legacy (viste secondarie).
- assets.html: filtro per voce, form con selettore Voce + Sottoclasse (+ '+ aggiungi'),
  lista/dettaglio mostrano 'voce · sottoclasse', stat-chips per voce.
- FIX bug pre-esistente: apostrofo non escapato ('l'inventario') che ROMPEVA tutto il JS
  della pagina Inventario. + cache-buster common-bi.js (era a v=20260618, non bumpato dal fix C3).
Help + KB + i18n. Verifiche prod: subclasses 200, create voce/legacy OK, validazione 422,
JS valido (node --check), migrazione idempotente. version 1.18.4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-06-16 19:59:35 +02:00
co-authored by Claude Opus 4.8
parent e281aa4970
commit 30f4614b37
41 changed files with 370 additions and 173 deletions
+119 -3
View File
@@ -37,9 +37,12 @@ class AssetController extends BaseController
$total = Database::count('assets', $where, $params); $total = Database::count('assets', $where, $params);
$assets = Database::fetchAll( $assets = Database::fetchAll(
"SELECT a.*, u.full_name as owner_name "SELECT a.*, u.full_name as owner_name,
sc.label AS subclass_label, v.label AS voce_label
FROM assets a FROM assets a
LEFT JOIN users u ON u.id = a.owner_user_id LEFT JOIN users u ON u.id = a.owner_user_id
LEFT JOIN inventory_subclassi sc ON sc.id = a.subclass_id
LEFT JOIN cfg_inventory_voci v ON v.voce_code = a.voce_code
WHERE a.{$where} WHERE a.{$where}
ORDER BY a.criticality DESC, a.name ORDER BY a.criticality DESC, a.name
LIMIT {$pagination['per_page']} OFFSET {$pagination['offset']}", LIMIT {$pagination['per_page']} OFFSET {$pagination['offset']}",
@@ -52,12 +55,28 @@ class AssetController extends BaseController
public function create(): void public function create(): void
{ {
$this->requireOrgRole(['org_admin', 'compliance_manager']); $this->requireOrgRole(['org_admin', 'compliance_manager']);
$this->validateRequired(['name', 'asset_type']); $this->validateRequired(['name']);
$voce = $this->getParam('voce_code');
if (!$voce) {
// retro-compatibilità: deriva la voce dal vecchio asset_type (UI non ancora aggiornata / import)
$at = (string) $this->getParam('asset_type');
if (in_array($at, ['software', 'service', 'data'], true)) { $voce = 'ID.AM-02'; }
elseif (in_array($at, ['hardware', 'network', 'facility'], true)) { $voce = 'ID.AM-01'; }
}
if (!in_array($voce, ['ID.AM-01', 'ID.AM-02'], true)) {
$this->jsonError('Voce inventario obbligatoria (ID.AM-01 o ID.AM-02)', 422, 'INVALID_VOCE');
}
$subclassId = $this->validateSubclass($this->getParam('subclass_id'), $voce);
// asset_type resta come campo legacy (viste secondarie): usa quello passato o deriva dalla voce
$assetType = $this->getParam('asset_type') ?: ($voce === 'ID.AM-01' ? 'hardware' : 'service');
$assetId = Database::insert('assets', [ $assetId = Database::insert('assets', [
'organization_id' => $this->getCurrentOrgId(), 'organization_id' => $this->getCurrentOrgId(),
'name' => trim($this->getParam('name')), 'name' => trim($this->getParam('name')),
'asset_type' => $this->getParam('asset_type'), 'asset_type' => $assetType,
'voce_code' => $voce,
'subclass_id' => $subclassId,
'category' => $this->getParam('category'), 'category' => $this->getParam('category'),
'description' => $this->getParam('description'), 'description' => $this->getParam('description'),
'criticality' => $this->getParam('criticality', 'medium'), 'criticality' => $this->getParam('criticality', 'medium'),
@@ -119,6 +138,8 @@ class AssetController extends BaseController
$type = strtolower((string) ($a['asset_type'] ?? 'service')); $type = strtolower((string) ($a['asset_type'] ?? 'service'));
if (!in_array($type, $validType, true)) $type = 'service'; if (!in_array($type, $validType, true)) $type = 'service';
// C4: voce (2 valori) derivata dal tipo legacy per gli asset importati
$voce = in_array($type, ['hardware', 'network', 'facility'], true) ? 'ID.AM-01' : 'ID.AM-02';
// Scoring automatico GV.OC-04 da euristica sui campi CMDB // Scoring automatico GV.OC-04 da euristica sui campi CMDB
$criteria = AssetScoringService::inferCriteria($a); $criteria = AssetScoringService::inferCriteria($a);
@@ -130,6 +151,7 @@ class AssetController extends BaseController
'organization_id' => $orgId, 'organization_id' => $orgId,
'name' => $name, 'name' => $name,
'asset_type' => $type, 'asset_type' => $type,
'voce_code' => $voce,
'category' => $a['category'] ?? null, 'category' => $a['category'] ?? null,
'description' => $a['description'] ?? null, 'description' => $a['description'] ?? null,
'criticality' => $sc['criticality'], 'criticality' => $sc['criticality'],
@@ -216,6 +238,21 @@ class AssetController extends BaseController
$updates['dependencies'] = json_encode($this->getParam('dependencies')); $updates['dependencies'] = json_encode($this->getParam('dependencies'));
} }
// Voce/sottoclasse (Epic C / C4) — con validazione (no loop generico)
if ($this->hasParam('voce_code')) {
$voce = $this->getParam('voce_code');
if (!in_array($voce, ['ID.AM-01', 'ID.AM-02'], true)) {
$this->jsonError('Voce inventario non valida', 422, 'INVALID_VOCE');
}
$updates['voce_code'] = $voce;
if ($this->hasParam('subclass_id')) {
$updates['subclass_id'] = $this->validateSubclass($this->getParam('subclass_id'), $voce);
}
} elseif ($this->hasParam('subclass_id')) {
$cur = Database::fetchOne('SELECT voce_code FROM assets WHERE id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]);
$updates['subclass_id'] = $this->validateSubclass($this->getParam('subclass_id'), $cur['voce_code'] ?? '');
}
if (!empty($updates)) { if (!empty($updates)) {
Database::update('assets', $updates, 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]); Database::update('assets', $updates, 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]);
$this->logAudit('asset_updated', 'asset', $id, $updates); $this->logAudit('asset_updated', 'asset', $id, $updates);
@@ -224,6 +261,85 @@ class AssetController extends BaseController
$this->jsonSuccess($updates, 'Asset aggiornato'); $this->jsonSuccess($updates, 'Asset aggiornato');
} }
/**
* Valida una sottoclasse per la voce indicata: deve esistere ed essere o di
* sistema (organization_id NULL) o dell'org corrente, e appartenere alla voce.
* Ritorna l'id (int) oppure null se non fornita.
*/
private function validateSubclass($id, string $voce): ?int
{
$id = (int) $id;
if ($id <= 0) { return null; }
$row = Database::fetchOne(
'SELECT id FROM inventory_subclassi
WHERE id = ? AND voce_code = ? AND (organization_id = ? OR organization_id IS NULL)',
[$id, $voce, $this->getCurrentOrgId()]
);
if (!$row) { $this->jsonError('Sottoclasse non valida per la voce selezionata', 422, 'INVALID_SUBCLASS'); }
return $id;
}
/**
* GET /api/assets/subclasses — le 2 voci canoniche + le sottoclassi visibili
* (default di sistema + quelle dell'org), per i selettori dell'inventario.
*/
public function subclasses(): void
{
$this->requireOrgAccess();
$orgId = $this->getCurrentOrgId();
$voci = Database::fetchAll('SELECT voce_code, label, descr, ord FROM cfg_inventory_voci ORDER BY ord');
$subs = Database::fetchAll(
'SELECT id, voce_code, label, (organization_id IS NULL) AS is_default, ord
FROM inventory_subclassi
WHERE organization_id IS NULL OR organization_id = ?
ORDER BY voce_code, ord, label',
[$orgId]
);
$byVoce = [];
foreach ($subs as $s) {
$byVoce[$s['voce_code']][] = [
'id' => (int) $s['id'], 'label' => $s['label'], 'is_default' => ((int) $s['is_default'] === 1),
];
}
$out = [];
foreach ($voci as $v) {
$out[] = [
'voce_code' => $v['voce_code'], 'label' => $v['label'], 'descr' => $v['descr'],
'subclassi' => $byVoce[$v['voce_code']] ?? [],
];
}
$this->jsonSuccess(['voci' => $out]);
}
/**
* POST /api/assets/subclasses { voce_code*, label* }
* Aggiunge una sottoclasse ORGANIZZATIVA (org-scoped). Le due voci principali
* NON sono modificabili dall'utente (restano due e solo due).
*/
public function addSubclass(): void
{
$this->requireOrgRole(['org_admin', 'compliance_manager']);
$body = $this->getJsonBody();
$voce = $body['voce_code'] ?? '';
$label = trim((string) ($body['label'] ?? ''));
if (!in_array($voce, ['ID.AM-01', 'ID.AM-02'], true)) {
$this->jsonError('Voce inventario non valida', 422, 'INVALID_VOCE');
}
if ($label === '' || mb_strlen($label) > 120) {
$this->jsonError('Etichetta sottoclasse obbligatoria (max 120 caratteri)', 422, 'INVALID_LABEL');
}
$orgId = $this->getCurrentOrgId();
$dup = Database::fetchOne(
'SELECT id FROM inventory_subclassi WHERE voce_code = ? AND label = ? AND (organization_id = ? OR organization_id IS NULL)',
[$voce, $label, $orgId]
);
if ($dup) { $this->jsonError('Sottoclasse già esistente', 409, 'DUPLICATE'); }
$id = Database::insert('inventory_subclassi', [
'organization_id' => $orgId, 'voce_code' => $voce, 'label' => $label, 'ord' => 99,
]);
$this->jsonSuccess(['id' => $id, 'voce_code' => $voce, 'label' => $label], 'Sottoclasse aggiunta', 201);
}
public function delete(int $id): void public function delete(int $id): void
{ {
$this->requireOrgRole(['org_admin']); $this->requireOrgRole(['org_admin']);
+40
View File
@@ -0,0 +1,40 @@
-- =====================================================================
-- 050 — Inventario a DUE voci + sottoclassi configurabili (Epic C / C4)
-- =====================================================================
-- Simon C4: nell'inventario DUE sole voci principali (ID.AM-01 Hardware,
-- ID.AM-02 Software/Servizi/Sistemi); il dettaglio diventa SOTTOCLASSE
-- (config: default di sistema + create dall'utente). Riconduzione concordata:
-- hardware/network/facility -> ID.AM-01 ; software/service/data -> ID.AM-02 ;
-- personnel -> Organigramma (nessun asset personnel presente).
--
-- Additivo: aggiunge cfg_inventory_voci, inventory_subclassi e
-- assets.voce_code/subclass_id; NON tocca asset_type (resta come legacy per le
-- viste secondarie). Migrazione dati inclusa (vedi runner). Reversibile.
-- Runner-safe: solo CREATE/ALTER bare (no DELIMITER).
-- =====================================================================
CREATE TABLE IF NOT EXISTS cfg_inventory_voci (
voce_code VARCHAR(16) NOT NULL,
label VARCHAR(120) NOT NULL,
descr TEXT NULL,
ord INT NOT NULL,
PRIMARY KEY (voce_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS inventory_subclassi (
id INT NOT NULL AUTO_INCREMENT,
organization_id INT NULL, -- NULL = default di sistema; valorizzato = sottoclasse dell'org
voce_code VARCHAR(16) NOT NULL,
label VARCHAR(120) NOT NULL,
ord INT NOT NULL DEFAULT 0,
PRIMARY KEY (id),
UNIQUE KEY uq_subclass (organization_id, voce_code, label),
KEY idx_subclass_org (organization_id),
KEY idx_subclass_voce (voce_code),
CONSTRAINT fk_subclass_voce FOREIGN KEY (voce_code) REFERENCES cfg_inventory_voci (voce_code),
CONSTRAINT fk_subclass_org FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
ALTER TABLE assets ADD COLUMN voce_code VARCHAR(16) NULL DEFAULT NULL;
ALTER TABLE assets ADD COLUMN subclass_id INT NULL DEFAULT NULL;
ALTER TABLE assets ADD CONSTRAINT fk_assets_subclass FOREIGN KEY (subclass_id) REFERENCES inventory_subclassi (id) ON DELETE SET NULL;
+3 -3
View File
@@ -70,9 +70,9 @@
</script> </script>
<!-- Stessa logica della app: api.js + common.js (helper) + common-bi.js (override BI) --> <!-- Stessa logica della app: api.js + common.js (helper) + common-bi.js (override BI) -->
<script src="js/api.js?v=20260624"></script> <script src="js/api.js?v=20260625"></script>
<script src="js/common.js?v=20260624"></script> <script src="js/common.js?v=20260625"></script>
<script src="js/common-bi.js?v=20260618"></script> <script src="js/common-bi.js?v=20260625"></script>
<script> <script>
// Renderizza la sidebar BI (loadSidebar è stato ridefinito da common-bi.js) // Renderizza la sidebar BI (loadSidebar è stato ridefinito da common-bi.js)
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
+4 -4
View File
@@ -1088,10 +1088,10 @@
</main> </main>
</div> </div>
<script src="js/api.js?v=20260624"></script> <script src="js/api.js?v=20260625"></script>
<script src="js/common.js?v=20260624"></script> <script src="js/common.js?v=20260625"></script>
<script src="js/i18n.js?v=20260624"></script> <script src="js/i18n.js?v=20260625"></script>
<script src="js/help.js?v=20260624"></script> <script src="js/help.js?v=20260625"></script>
<script> <script>
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); loadSidebar();
+5 -5
View File
@@ -154,8 +154,8 @@
</main> </main>
</div> </div>
<script src="/js/api.js?v=20260624"></script> <script src="/js/api.js?v=20260625"></script>
<script src="/js/common.js?v=20260624"></script> <script src="/js/common.js?v=20260625"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js). <!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI, common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
riusando tutti gli helper di common.js (zero backend). --> riusando tutti gli helper di common.js (zero backend). -->
@@ -165,9 +165,9 @@
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
} }
</script> </script>
<script src="/js/common-bi.js?v=20260618"></script> <script src="/js/common-bi.js?v=20260625"></script>
<script src="/js/i18n.js?v=20260624"></script> <script src="/js/i18n.js?v=20260625"></script>
<script src="/js/help.js?v=20260624"></script> <script src="/js/help.js?v=20260625"></script>
<script> <script>
// ── Auth check ─────────────────────────────────────────── // ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
+67 -30
View File
@@ -331,14 +331,9 @@
<!-- Filters --> <!-- Filters -->
<div class="filters-bar"> <div class="filters-bar">
<select id="filter-type" onchange="applyFilters()" aria-label="Filtra per tipo"> <select id="filter-type" onchange="applyFilters()" aria-label="Filtra per tipo">
<option value="">Tutti i tipi</option> <option value="">Tutte le voci</option>
<option value="hardware">Hardware</option> <option value="ID.AM-01">Hardware (ID.AM-01)</option>
<option value="software">Software</option> <option value="ID.AM-02">Software/Servizi/Sistemi (ID.AM-02)</option>
<option value="network">Rete</option>
<option value="data">Dati</option>
<option value="service">Servizio</option>
<option value="personnel">Personale</option>
<option value="facility">Struttura</option>
</select> </select>
<select id="filter-criticality" onchange="applyFilters()" aria-label="Filtra per criticità"> <select id="filter-criticality" onchange="applyFilters()" aria-label="Filtra per criticità">
<option value="">Tutte le criticita'</option> <option value="">Tutte le criticita'</option>
@@ -366,8 +361,8 @@
</main> </main>
</div> </div>
<script src="/js/api.js?v=20260624"></script> <script src="/js/api.js?v=20260625"></script>
<script src="/js/common.js?v=20260624"></script> <script src="/js/common.js?v=20260625"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js). <!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI, common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
riusando tutti gli helper di common.js (zero backend). --> riusando tutti gli helper di common.js (zero backend). -->
@@ -377,15 +372,16 @@
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
} }
</script> </script>
<script src="/js/common-bi.js?v=20260618"></script> <script src="/js/common-bi.js?v=20260625"></script>
<script src="/js/i18n.js?v=20260624"></script> <script src="/js/i18n.js?v=20260625"></script>
<script src="/js/help.js?v=20260624"></script> <script src="/js/help.js?v=20260625"></script>
<script> <script>
// ── Auth & Init ───────────────────────────────────────── // ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); loadSidebar();
I18n.init(); I18n.init();
HelpSystem.init(); HelpSystem.init();
loadSubclasses(); // C4 — voci+sottoclassi per i selettori (fire-and-forget, pronto al primo "Nuovo")
// ── Labels ────────────────────────────────────────────── // ── Labels ──────────────────────────────────────────────
const typeLabels = { const typeLabels = {
@@ -411,6 +407,42 @@
decommissioned: 'Dismesso' decommissioned: 'Dismesso'
}; };
// ── Voci / sottoclassi (Epic C / C4) ────────────────────
const VOCE_LABELS = { 'ID.AM-01': 'Hardware', 'ID.AM-02': 'Software/Servizi/Sistemi' };
let SUBCLASSES = []; // [{voce_code,label,subclassi:[{id,label,is_default}]}]
function assetTipo(a) {
const v = a.voce_label || VOCE_LABELS[a.voce_code] || (typeLabels[a.asset_type] || a.asset_type || '-');
return a.subclass_label ? (v + ' · ' + a.subclass_label) : v;
}
async function loadSubclasses() {
try { const r = await api.listAssetSubclasses(); if (r && r.success) SUBCLASSES = (r.data && r.data.voci) || []; }
catch (e) { SUBCLASSES = []; }
}
function voceOptionsHtml(sel) {
return ['ID.AM-01', 'ID.AM-02'].map(v =>
`<option value="${v}" ${sel === v ? 'selected' : ''}>${v} — ${VOCE_LABELS[v]}</option>`).join('');
}
function subclassOptionsHtml(voce, sel) {
const grp = SUBCLASSES.find(g => g.voce_code === voce);
const subs = grp ? grp.subclassi : [];
return '<option value="">— nessuna —</option>' + subs.map(s =>
`<option value="${s.id}" ${String(sel) === String(s.id) ? 'selected' : ''}>${escapeHtml(s.label)}</option>`).join('');
}
function onVoceChange() {
const voce = document.getElementById('asset-voce').value;
document.getElementById('asset-subclass').innerHTML = subclassOptionsHtml(voce, '');
}
async function addSubclassPrompt() {
const voce = document.getElementById('asset-voce').value;
const label = (prompt('Nuova sottoclasse per «' + VOCE_LABELS[voce] + '»:') || '').trim();
if (!label) return;
try {
const r = await api.addAssetSubclass({ voce_code: voce, label: label });
if (r && r.success) { await loadSubclasses(); document.getElementById('asset-subclass').innerHTML = subclassOptionsHtml(voce, r.data.id); showNotification('Sottoclasse aggiunta.', 'success'); }
else showNotification((r && r.message) || 'Errore.', 'warning');
} catch (e) { showNotification('Errore aggiunta sottoclasse.', 'warning'); }
}
// ── State ─────────────────────────────────────────────── // ── State ───────────────────────────────────────────────
let allAssets = []; let allAssets = [];
@@ -423,7 +455,6 @@
const type = document.getElementById('filter-type').value; const type = document.getElementById('filter-type').value;
const criticality = document.getElementById('filter-criticality').value; const criticality = document.getElementById('filter-criticality').value;
const status = document.getElementById('filter-status').value; const status = document.getElementById('filter-status').value;
if (type) params.asset_type = type;
if (criticality) params.criticality = criticality; if (criticality) params.criticality = criticality;
if (status) params.status = status; if (status) params.status = status;
@@ -431,13 +462,14 @@
const result = await api.listAssets(params); const result = await api.listAssets(params);
if (result.success) { if (result.success) {
allAssets = result.data || []; allAssets = result.data || [];
updateStats(allAssets); const view = type ? allAssets.filter(a => a.voce_code === type) : allAssets;
renderAssets(allAssets); updateStats(view);
renderAssets(view);
} else { } else {
container.innerHTML = '<div class="empty-state-box"><h4>Errore nel caricamento</h4><p>' + escapeHtml(result.message || '') + '</p></div>'; container.innerHTML = '<div class="empty-state-box"><h4>Errore nel caricamento</h4><p>' + escapeHtml(result.message || '') + '</p></div>';
} }
} catch (e) { } catch (e) {
container.innerHTML = '<div class="empty-state-box"><h4>Errore di connessione</h4><p>Impossibile caricare l'inventario.</p></div>'; container.innerHTML = '<div class="empty-state-box"><h4>Errore di connessione</h4><p>Impossibile caricare l\'inventario.</p></div>';
} }
} }
@@ -455,7 +487,7 @@
let criticalCount = 0; let criticalCount = 0;
let decommCount = 0; let decommCount = 0;
assets.forEach(a => { assets.forEach(a => {
const t = a.asset_type || 'unknown'; const t = a.voce_label || VOCE_LABELS[a.voce_code] || 'Non classificato';
typeCount[t] = (typeCount[t] || 0) + 1; typeCount[t] = (typeCount[t] || 0) + 1;
if (a.criticality === 'critical') criticalCount++; if (a.criticality === 'critical') criticalCount++;
if (a.status === 'decommissioned') decommCount++; if (a.status === 'decommissioned') decommCount++;
@@ -469,7 +501,7 @@
// Type chips // Type chips
const chipsEl = document.getElementById('stat-type-chips'); const chipsEl = document.getElementById('stat-type-chips');
chipsEl.innerHTML = Object.entries(typeCount).map(([type, count]) => chipsEl.innerHTML = Object.entries(typeCount).map(([type, count]) =>
`<span class="type-chip"><strong>${count}</strong> ${typeLabels[type] || type}</span>` `<span class="type-chip"><strong>${count}</strong> ${escapeHtml(type)}</span>`
).join(''); ).join('');
} }
@@ -508,7 +540,7 @@
html += ` html += `
<tr onclick="showAssetDetail(${asset.id})" title="Visualizza dettagli"> <tr onclick="showAssetDetail(${asset.id})" title="Visualizza dettagli">
<td style="font-weight:500;">${escapeHtml(asset.name)}</td> <td style="font-weight:500;">${escapeHtml(asset.name)}</td>
<td>${typeLabels[asset.asset_type] || asset.asset_type || '-'}</td> <td>${escapeHtml(assetTipo(asset))}</td>
<td>${escapeHtml(asset.category || '-')}</td> <td>${escapeHtml(asset.category || '-')}</td>
<td><span class="criticality-badge ${crit}">${criticalityLabels[crit] || crit}</span></td> <td><span class="criticality-badge ${crit}">${criticalityLabels[crit] || crit}</span></td>
<td>${relevanceBadge(asset)}</td> <td>${relevanceBadge(asset)}</td>
@@ -656,8 +688,8 @@
showModal(escapeHtml(a.name), ` showModal(escapeHtml(a.name), `
<div class="detail-grid"> <div class="detail-grid">
<div class="detail-field"> <div class="detail-field">
<div class="detail-field-label">Tipo</div> <div class="detail-field-label">Voce / sottoclasse</div>
<div class="detail-field-value">${typeLabels[a.asset_type] || a.asset_type || '-'}</div> <div class="detail-field-value">${escapeHtml(assetTipo(a))}</div>
</div> </div>
<div class="detail-field"> <div class="detail-field">
<div class="detail-field-label">Categoria</div> <div class="detail-field-label">Categoria</div>
@@ -773,9 +805,9 @@
function showAssetModal(title, asset, onSave) { function showAssetModal(title, asset, onSave) {
const a = asset || {}; const a = asset || {};
const typeOptions = Object.entries(typeLabels).map(([val, label]) => const voceSel = a.voce_code || 'ID.AM-01';
`<option value="${val}" ${a.asset_type === val ? 'selected' : ''}>${label}</option>` const voceOpts = voceOptionsHtml(a.voce_code);
).join(''); const subOpts = subclassOptionsHtml(voceSel, a.subclass_id);
const critOptions = Object.entries(criticalityLabels).map(([val, label]) => const critOptions = Object.entries(criticalityLabels).map(([val, label]) =>
`<option value="${val}" ${a.criticality === val ? 'selected' : ''}>${label}</option>` `<option value="${val}" ${a.criticality === val ? 'selected' : ''}>${label}</option>`
@@ -795,14 +827,18 @@
</div> </div>
<div class="form-grid-2"> <div class="form-grid-2">
<div class="form-group"> <div class="form-group">
<label for="asset-type" class="form-label">Tipo *</label> <label for="asset-voce" class="form-label">Voce inventario *</label>
<select class="form-select" id="asset-type">${typeOptions}</select> <select class="form-select" id="asset-voce" onchange="onVoceChange()">${voceOpts}</select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="asset-category" class="form-label">Categoria</label> <label for="asset-subclass" class="form-label">Sottoclasse <a href="#" onclick="addSubclassPrompt();return false;" style="font-weight:400;font-size:.78rem;margin-left:6px;">+ aggiungi</a></label>
<input type="text" class="form-input" id="asset-category" value="${escapeHtml(a.category || '')}" placeholder="Es: Server, Database..."> <select class="form-select" id="asset-subclass">${subOpts}</select>
</div> </div>
</div> </div>
<div class="form-group">
<label for="asset-category" class="form-label">Categoria (etichetta libera)</label>
<input type="text" class="form-input" id="asset-category" value="${escapeHtml(a.category || '')}" placeholder="Opzionale: nota libera">
</div>
<div class="form-group"> <div class="form-group">
<label for="asset-description" 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> <textarea class="form-input" id="asset-description" rows="2">${escapeHtml(a.description || '')}</textarea>
@@ -875,7 +911,8 @@
const data = { const data = {
name: name, name: name,
asset_type: document.getElementById('asset-type').value, voce_code: document.getElementById('asset-voce').value,
subclass_id: document.getElementById('asset-subclass').value || null,
category: document.getElementById('asset-category').value.trim() || null, category: document.getElementById('asset-category').value.trim() || null,
description: document.getElementById('asset-description').value.trim() || null, description: document.getElementById('asset-description').value.trim() || null,
criticality: document.getElementById('asset-criticality').value, criticality: document.getElementById('asset-criticality').value,
+5 -5
View File
@@ -349,14 +349,14 @@
</main> </main>
</div> </div>
<script src="js/api.js?v=20260624"></script> <script src="js/api.js?v=20260625"></script>
<script src="js/common.js?v=20260624"></script> <script src="js/common.js?v=20260625"></script>
<!-- Bootstrap Italia bundle + override sidebar BI (common-bi.js dopo common.js: ridefinisce solo loadSidebar) --> <!-- 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 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>if (window.bootstrap && bootstrap.loadFonts) { bootstrap.loadFonts('vendor/bootstrap-italia/dist/fonts'); }</script>
<script src="js/common-bi.js?v=20260618"></script> <script src="js/common-bi.js?v=20260625"></script>
<script src="js/i18n.js?v=20260624"></script> <script src="js/i18n.js?v=20260625"></script>
<script src="js/help.js?v=20260624"></script> <script src="js/help.js?v=20260625"></script>
<script> <script>
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
+5 -5
View File
@@ -183,12 +183,12 @@
</div> </div>
</div> </div>
<script src="/js/api.js?v=20260624"></script> <script src="/js/api.js?v=20260625"></script>
<script src="/js/common.js?v=20260624"></script> <script src="/js/common.js?v=20260625"></script>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script src="/js/common-bi.js?v=20260618"></script> <script src="/js/common-bi.js?v=20260625"></script>
<script src="/js/i18n.js?v=20260624"></script> <script src="/js/i18n.js?v=20260625"></script>
<script src="/js/help.js?v=20260624"></script> <script src="/js/help.js?v=20260625"></script>
<script src="/js/competenze.js?v=20260617"></script> <script src="/js/competenze.js?v=20260617"></script>
</body> </body>
</html> </html>
+4 -4
View File
@@ -382,8 +382,8 @@
</main> </main>
</div> </div>
<script src="/js/api.js?v=20260624"></script> <script src="/js/api.js?v=20260625"></script>
<script src="/js/common.js?v=20260624"></script> <script src="/js/common.js?v=20260625"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js). <!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI, common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
riusando tutti gli helper di common.js (zero backend). --> riusando tutti gli helper di common.js (zero backend). -->
@@ -393,8 +393,8 @@
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
} }
</script> </script>
<script src="/js/common-bi.js?v=20260618"></script> <script src="/js/common-bi.js?v=20260625"></script>
<script src="/js/i18n.js?v=20260624"></script> <script src="/js/i18n.js?v=20260625"></script>
<script> <script>
const SPRITE = '/vendor/bootstrap-italia/dist/svg/sprites.svg'; 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>`; } function ico(name, cls) { return `<svg class="ico ${cls||''}" aria-hidden="true"><use href="${SPRITE}#${name}"></use></svg>`; }
+5 -5
View File
@@ -143,8 +143,8 @@
</main> </main>
</div> </div>
<script src="/js/api.js?v=20260624"></script> <script src="/js/api.js?v=20260625"></script>
<script src="/js/common.js?v=20260624"></script> <script src="/js/common.js?v=20260625"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js). <!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI, common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
riusando tutti gli helper di common.js (zero backend). --> riusando tutti gli helper di common.js (zero backend). -->
@@ -154,9 +154,9 @@
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
} }
</script> </script>
<script src="/js/common-bi.js?v=20260618"></script> <script src="/js/common-bi.js?v=20260625"></script>
<script src="/js/i18n.js?v=20260624"></script> <script src="/js/i18n.js?v=20260625"></script>
<script src="/js/help.js?v=20260624"></script> <script src="/js/help.js?v=20260625"></script>
<script> <script>
// ── Auth check ─────────────────────────────────────────── // ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
+1 -1
View File
@@ -73,7 +73,7 @@
</script> </script>
<!-- Stessa logica della forgot-password.html: ZERO modifiche backend --> <!-- Stessa logica della forgot-password.html: ZERO modifiche backend -->
<script src="js/api.js?v=20260624"></script> <script src="js/api.js?v=20260625"></script>
<script> <script>
const form = document.getElementById('forgot-form'); const form = document.getElementById('forgot-form');
const err = document.getElementById('err'); const err = document.getElementById('err');
+5 -5
View File
@@ -1142,8 +1142,8 @@
</main> </main>
</div> </div>
<script src="/js/api.js?v=20260624"></script> <script src="/js/api.js?v=20260625"></script>
<script src="/js/common.js?v=20260624"></script> <script src="/js/common.js?v=20260625"></script>
<!-- Bootstrap Italia bundle (accordion) + override sidebar BI (common-bi.js). <!-- Bootstrap Italia bundle (accordion) + override sidebar BI (common-bi.js).
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI. --> 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 src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
@@ -1152,9 +1152,9 @@
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
} }
</script> </script>
<script src="/js/common-bi.js?v=20260618"></script> <script src="/js/common-bi.js?v=20260625"></script>
<script src="/js/i18n.js?v=20260624"></script> <script src="/js/i18n.js?v=20260625"></script>
<script src="/js/help.js?v=20260624"></script> <script src="/js/help.js?v=20260625"></script>
<script> <script>
// La guida è accessibile anche da non autenticati (utile per onboarding) // La guida è accessibile anche da non autenticati (utile per onboarding)
// ma se sei loggato carichi sidebar + i18n normalmente. Stessa logica // ma se sei loggato carichi sidebar + i18n normalmente. Stessa logica
+5 -5
View File
@@ -351,8 +351,8 @@
</main> </main>
</div> </div>
<script src="/js/api.js?v=20260624"></script> <script src="/js/api.js?v=20260625"></script>
<script src="/js/common.js?v=20260624"></script> <script src="/js/common.js?v=20260625"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js). <!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI, common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
riusando tutti gli helper di common.js (zero backend). --> riusando tutti gli helper di common.js (zero backend). -->
@@ -362,9 +362,9 @@
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
} }
</script> </script>
<script src="/js/common-bi.js?v=20260618"></script> <script src="/js/common-bi.js?v=20260625"></script>
<script src="/js/i18n.js?v=20260624"></script> <script src="/js/i18n.js?v=20260625"></script>
<script src="/js/help.js?v=20260624"></script> <script src="/js/help.js?v=20260625"></script>
<script> <script>
// ── Auth & Init ────────────────────────────────────────────── // ── Auth & Init ──────────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
+2
View File
@@ -421,6 +421,8 @@ $actionMap = [
'GET:scoringGrid' => 'scoringGrid', 'GET:scoringGrid' => 'scoringGrid',
'GET:relevantSystems' => 'relevantSystems', 'GET:relevantSystems' => 'relevantSystems',
'GET:dependencyMap' => 'dependencyMap', 'GET:dependencyMap' => 'dependencyMap',
'GET:subclasses' => 'subclasses', // Epic C / C4 — voci + sottoclassi
'POST:subclasses' => 'addSubclass', // Epic C / C4 — aggiunta sottoclasse org
'GET:{id}' => 'get', 'GET:{id}' => 'get',
'PUT:{id}' => 'update', 'PUT:{id}' => 'update',
'DELETE:{id}' => 'delete', 'DELETE:{id}' => 'delete',
+2 -2
View File
@@ -910,8 +910,8 @@ curl -H <span class="str">"X-API-Key: nis2_TUA_CHIAVE"</span> \
</main> </main>
<script src="js/common.js?v=20260624"></script> <script src="js/common.js?v=20260625"></script>
<script src="js/i18n.js?v=20260624"></script> <script src="js/i18n.js?v=20260625"></script>
<script> <script>
function showTab(id) { function showTab(id) {
document.querySelectorAll('.tab-btn').forEach((b, i) => { document.querySelectorAll('.tab-btn').forEach((b, i) => {
+5 -5
View File
@@ -184,8 +184,8 @@
</main> </main>
</div> </div>
<script src="/js/api.js?v=20260624"></script> <script src="/js/api.js?v=20260625"></script>
<script src="/js/common.js?v=20260624"></script> <script src="/js/common.js?v=20260625"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js). <!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI, common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
riusando tutti gli helper di common.js (zero backend). --> riusando tutti gli helper di common.js (zero backend). -->
@@ -195,9 +195,9 @@
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
} }
</script> </script>
<script src="/js/common-bi.js?v=20260618"></script> <script src="/js/common-bi.js?v=20260625"></script>
<script src="/js/i18n.js?v=20260624"></script> <script src="/js/i18n.js?v=20260625"></script>
<script src="/js/help.js?v=20260624"></script> <script src="/js/help.js?v=20260625"></script>
<script src="/js/isms.js"></script> <script src="/js/isms.js"></script>
</body> </body>
</html> </html>
+2
View File
@@ -384,6 +384,8 @@ class NIS2API {
scoreAsset(id, criteria) { return this.post(`/assets/${id}/score`, { criteria }); } scoreAsset(id, criteria) { return this.post(`/assets/${id}/score`, { criteria }); }
listRelevantSystems() { return this.get('/assets/relevantSystems'); } listRelevantSystems() { return this.get('/assets/relevantSystems'); }
importAssets(data) { return this.post('/assets/import', data); } // P2 import CMDB/CSV importAssets(data) { return this.post('/assets/import', data); } // P2 import CMDB/CSV
listAssetSubclasses() { return this.get('/assets/subclasses'); } // C4 — voci + sottoclassi
addAssetSubclass(data) { return this.post('/assets/subclasses', data); } // C4 — nuova sottoclasse org
getControlsMonitoring() { return this.get('/audit/controlsMonitoring'); } getControlsMonitoring() { return this.get('/audit/controlsMonitoring'); }
getAcnRequirements() { return this.get('/audit/acnRequirements'); } // requisiti ACN per org getAcnRequirements() { return this.get('/audit/acnRequirements'); } // requisiti ACN per org
updateAcnRequirement(id, status, note) { return this.put(`/audit/acnRequirements/${id}`, { status, evidence_note: note }); } updateAcnRequirement(id, status, note) { return this.put(`/audit/acnRequirements/${id}`, { status, evidence_note: note }); }
+5 -5
View File
@@ -689,12 +689,12 @@ const HelpSystem = (function () {
intro: 'Il modulo Inventario consente di catalogare e gestire i beni ICT (hardware e software) dell\'organizzazione, fondamentale per la gestione dei rischi e la sicurezza delle reti e dei sistemi informativi ai sensi dell\'art. 24 del D.Lgs. 138/2024 (che recepisce l\'art. 21 della Direttiva (UE) 2022/2555).', intro: 'Il modulo Inventario consente di catalogare e gestire i beni ICT (hardware e software) dell\'organizzazione, fondamentale per la gestione dei rischi e la sicurezza delle reti e dei sistemi informativi ai sensi dell\'art. 24 del D.Lgs. 138/2024 (che recepisce l\'art. 21 della Direttiva (UE) 2022/2555).',
sections: [ sections: [
{ {
heading: 'Categorizzazione dei beni', heading: 'Le due voci dell\'inventario',
items: [ items: [
'<strong>Hardware</strong> - server, workstation, dispositivi di rete, dispositivi mobili, apparati di sicurezza.', 'La normativa prevede <strong>due sole voci principali</strong>: <strong>ID.AM-01 Hardware</strong> (apparati fisici: dispositivi IT, IoT, OT e mobili) e <strong>ID.AM-02 Software, servizi e sistemi</strong> (applicazioni commerciali/open-source/custom, servizi, sistemi, API).',
'<strong>Software</strong> - applicativi, sistemi operativi, database, middleware, servizi cloud.', 'Il dettaglio si esprime con le <strong>sottoclassi</strong>: alcune di default (es. Server, Dispositivi di rete, Applicazioni software, Basi dati e archivi) e altre che <strong>puoi aggiungere tu</strong> con "+ aggiungi" accanto al campo Sottoclasse.',
'<strong>Rete</strong> - reti LAN/WAN, collegamenti internet, VPN, segmenti di rete.', 'Le due voci principali non sono modificabili; le sottoclassi organizzano il tuo inventario sotto di esse.',
'<strong>Dati</strong> - archivi, database, backup, dati personali, dati critici per il business.' 'I flussi di rete (ID.AM-03, solo soggetti essenziali) e i servizi dei fornitori (ID.AM-04) sono registri distinti, non voci dell\'inventario asset.'
] ]
}, },
{ {
+5 -5
View File
@@ -151,8 +151,8 @@
</div> </div>
<!-- Stessa logica JS della pagina live (parita' funzionale assoluta, zero backend). --> <!-- Stessa logica JS della pagina live (parita' funzionale assoluta, zero backend). -->
<script src="/js/common.js?v=20260624"></script> <script src="/js/common.js?v=20260625"></script>
<script src="/js/api.js?v=20260624"></script> <script src="/js/api.js?v=20260625"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js). <!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI. --> 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 src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
@@ -161,9 +161,9 @@
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
} }
</script> </script>
<script src="/js/common-bi.js?v=20260618"></script> <script src="/js/common-bi.js?v=20260625"></script>
<script src="/js/i18n.js?v=20260624"></script> <script src="/js/i18n.js?v=20260625"></script>
<script src="/js/help.js?v=20260624"></script> <script src="/js/help.js?v=20260625"></script>
<script src="/js/kb.js"></script> <script src="/js/kb.js"></script>
<script> <script>
// Gate auth + chrome come le altre pagine -bi. // Gate auth + chrome come le altre pagine -bi.
+1 -1
View File
@@ -515,7 +515,7 @@ body { background: var(--bg-main); }
<div id="toast"></div> <div id="toast"></div>
<script src="js/api.js?v=20260624"></script> <script src="js/api.js?v=20260625"></script>
<script> <script>
// ══════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════
// CONFIG // CONFIG
+2 -2
View File
@@ -94,8 +94,8 @@
</script> </script>
<!-- Stessa logica della login attuale: ZERO modifiche backend --> <!-- Stessa logica della login attuale: ZERO modifiche backend -->
<script src="js/api.js?v=20260624"></script> <script src="js/api.js?v=20260625"></script>
<script src="js/common.js?v=20260624"></script> <script src="js/common.js?v=20260625"></script>
<script> <script>
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; } if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
+6 -6
View File
@@ -6,7 +6,7 @@
<title>Misure e Requisiti - NIS2 Agile</title> <title>Misure e Requisiti - NIS2 Agile</title>
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css. --> <!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css. -->
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css"> <link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<link rel="stylesheet" href="/css/style.css?v=20260624"> <link rel="stylesheet" href="/css/style.css?v=20260625">
<style> <style>
.mr-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:14px; font-size:.92rem; line-height:1.6; } .mr-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:14px; font-size:.92rem; line-height:1.6; }
.mr-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; } .mr-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
@@ -162,12 +162,12 @@
</div> </div>
</div> </div>
<script src="/js/api.js?v=20260624"></script> <script src="/js/api.js?v=20260625"></script>
<script src="/js/common.js?v=20260624"></script> <script src="/js/common.js?v=20260625"></script>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script src="/js/common-bi.js?v=20260618"></script> <script src="/js/common-bi.js?v=20260625"></script>
<script src="/js/i18n.js?v=20260624"></script> <script src="/js/i18n.js?v=20260625"></script>
<script src="/js/help.js?v=20260624"></script> <script src="/js/help.js?v=20260625"></script>
<script> <script>
'use strict'; 'use strict';
/* /*
+1 -1
View File
@@ -424,6 +424,6 @@ curl https://nis2.agile.software/api/services/status</pre>
</div> </div>
</div> </div>
<script src="js/common.js?v=20260624"></script> <script src="js/common.js?v=20260625"></script>
</body> </body>
</html> </html>
+5 -5
View File
@@ -112,8 +112,8 @@
</main> </main>
</div> </div>
<script src="/js/api.js?v=20260624"></script> <script src="/js/api.js?v=20260625"></script>
<script src="/js/common.js?v=20260624"></script> <script src="/js/common.js?v=20260625"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js). <!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI, common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
riusando tutti gli helper di common.js (zero backend). --> riusando tutti gli helper di common.js (zero backend). -->
@@ -123,9 +123,9 @@
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
} }
</script> </script>
<script src="/js/common-bi.js?v=20260618"></script> <script src="/js/common-bi.js?v=20260625"></script>
<script src="/js/i18n.js?v=20260624"></script> <script src="/js/i18n.js?v=20260625"></script>
<script src="/js/help.js?v=20260624"></script> <script src="/js/help.js?v=20260625"></script>
<script> <script>
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); loadSidebar();
+2 -2
View File
@@ -494,8 +494,8 @@
</script> </script>
<!-- Stessa logica della onboarding.html: ZERO modifiche backend --> <!-- Stessa logica della onboarding.html: ZERO modifiche backend -->
<script src="js/api.js?v=20260624"></script> <script src="js/api.js?v=20260625"></script>
<script src="js/common.js?v=20260624"></script> <script src="js/common.js?v=20260625"></script>
<script> <script>
// ── Auth check ────────────────────────────────────────────────── // ── Auth check ──────────────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
+5 -5
View File
@@ -138,12 +138,12 @@
</div> </div>
</div> </div>
<script src="/js/api.js?v=20260624"></script> <script src="/js/api.js?v=20260625"></script>
<script src="/js/common.js?v=20260624"></script> <script src="/js/common.js?v=20260625"></script>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script src="/js/common-bi.js?v=20260618"></script> <script src="/js/common-bi.js?v=20260625"></script>
<script src="/js/i18n.js?v=20260624"></script> <script src="/js/i18n.js?v=20260625"></script>
<script src="/js/help.js?v=20260624"></script> <script src="/js/help.js?v=20260625"></script>
<script src="/js/organigramma.js?v=20260617"></script> <script src="/js/organigramma.js?v=20260617"></script>
</body> </body>
</html> </html>
+5 -5
View File
@@ -333,8 +333,8 @@
</main> </main>
</div> </div>
<script src="/js/api.js?v=20260624"></script> <script src="/js/api.js?v=20260625"></script>
<script src="/js/common.js?v=20260624"></script> <script src="/js/common.js?v=20260625"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js). <!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI, common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
riusando tutti gli helper di common.js (zero backend). --> riusando tutti gli helper di common.js (zero backend). -->
@@ -344,9 +344,9 @@
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
} }
</script> </script>
<script src="/js/common-bi.js?v=20260618"></script> <script src="/js/common-bi.js?v=20260625"></script>
<script src="/js/i18n.js?v=20260624"></script> <script src="/js/i18n.js?v=20260625"></script>
<script src="/js/help.js?v=20260624"></script> <script src="/js/help.js?v=20260625"></script>
<script> <script>
// ── Auth & Init ───────────────────────────────────────── // ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
+5 -5
View File
@@ -131,12 +131,12 @@
</main> </main>
</div> </div>
<script src="/js/api.js?v=20260624"></script> <script src="/js/api.js?v=20260625"></script>
<script src="/js/common.js?v=20260624"></script> <script src="/js/common.js?v=20260625"></script>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script src="/js/common-bi.js?v=20260618"></script> <script src="/js/common-bi.js?v=20260625"></script>
<script src="/js/i18n.js?v=20260624"></script> <script src="/js/i18n.js?v=20260625"></script>
<script src="/js/help.js?v=20260624"></script> <script src="/js/help.js?v=20260625"></script>
<script src="/js/raci.js?v=20260619"></script> <script src="/js/raci.js?v=20260619"></script>
</body> </body>
</html> </html>
+2 -2
View File
@@ -268,8 +268,8 @@
</script> </script>
<!-- Stessa logica della register.html: ZERO modifiche backend --> <!-- Stessa logica della register.html: ZERO modifiche backend -->
<script src="js/api.js?v=20260624"></script> <script src="js/api.js?v=20260625"></script>
<script src="js/common.js?v=20260624"></script> <script src="js/common.js?v=20260625"></script>
<script> <script>
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; } if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
+5 -5
View File
@@ -443,8 +443,8 @@
</main> </main>
</div> </div>
<script src="/js/api.js?v=20260624"></script> <script src="/js/api.js?v=20260625"></script>
<script src="/js/common.js?v=20260624"></script> <script src="/js/common.js?v=20260625"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js). <!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI, common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
riusando tutti gli helper di common.js (zero backend). --> riusando tutti gli helper di common.js (zero backend). -->
@@ -454,9 +454,9 @@
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
} }
</script> </script>
<script src="/js/common-bi.js?v=20260618"></script> <script src="/js/common-bi.js?v=20260625"></script>
<script src="/js/i18n.js?v=20260624"></script> <script src="/js/i18n.js?v=20260625"></script>
<script src="/js/help.js?v=20260624"></script> <script src="/js/help.js?v=20260625"></script>
<script> <script>
// ── Auth & Init ───────────────────────────────────────── // ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
+5 -5
View File
@@ -141,12 +141,12 @@
</div> </div>
</div> </div>
<script src="/js/api.js?v=20260624"></script> <script src="/js/api.js?v=20260625"></script>
<script src="/js/common.js?v=20260624"></script> <script src="/js/common.js?v=20260625"></script>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script src="/js/common-bi.js?v=20260618"></script> <script src="/js/common-bi.js?v=20260625"></script>
<script src="/js/i18n.js?v=20260624"></script> <script src="/js/i18n.js?v=20260625"></script>
<script src="/js/help.js?v=20260624"></script> <script src="/js/help.js?v=20260625"></script>
<script> <script>
'use strict'; 'use strict';
/* /*
+5 -5
View File
@@ -494,8 +494,8 @@
</main> </main>
</div> </div>
<script src="/js/api.js?v=20260624"></script> <script src="/js/api.js?v=20260625"></script>
<script src="/js/common.js?v=20260624"></script> <script src="/js/common.js?v=20260625"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js). <!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI, common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
riusando tutti gli helper di common.js (zero backend). --> riusando tutti gli helper di common.js (zero backend). -->
@@ -505,9 +505,9 @@
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
} }
</script> </script>
<script src="/js/common-bi.js?v=20260618"></script> <script src="/js/common-bi.js?v=20260625"></script>
<script src="/js/i18n.js?v=20260624"></script> <script src="/js/i18n.js?v=20260625"></script>
<script src="/js/help.js?v=20260624"></script> <script src="/js/help.js?v=20260625"></script>
<script> <script>
// ── Auth & Init ────────────────────────────────────────────── // ── Auth & Init ──────────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
+5 -5
View File
@@ -672,8 +672,8 @@
</main> </main>
</div> </div>
<script src="/js/api.js?v=20260624"></script> <script src="/js/api.js?v=20260625"></script>
<script src="/js/common.js?v=20260624"></script> <script src="/js/common.js?v=20260625"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js). <!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI, common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
riusando tutti gli helper di common.js (zero backend). --> riusando tutti gli helper di common.js (zero backend). -->
@@ -683,9 +683,9 @@
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
} }
</script> </script>
<script src="/js/common-bi.js?v=20260618"></script> <script src="/js/common-bi.js?v=20260625"></script>
<script src="/js/i18n.js?v=20260624"></script> <script src="/js/i18n.js?v=20260625"></script>
<script src="/js/help.js?v=20260624"></script> <script src="/js/help.js?v=20260625"></script>
<script> <script>
// ── Auth check ─────────────────────────────────────────── // ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
+2 -2
View File
@@ -165,8 +165,8 @@
</main> </main>
</div> </div>
<script src="js/api.js?v=20260624"></script> <script src="js/api.js?v=20260625"></script>
<script src="js/common.js?v=20260624"></script> <script src="js/common.js?v=20260625"></script>
<script> <script>
// ── Auth check ─────────────────────────────────────────── // ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
+5 -5
View File
@@ -124,12 +124,12 @@
</div> </div>
</div> </div>
<script src="/js/api.js?v=20260624"></script> <script src="/js/api.js?v=20260625"></script>
<script src="/js/common.js?v=20260624"></script> <script src="/js/common.js?v=20260625"></script>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script src="/js/common-bi.js?v=20260618"></script> <script src="/js/common-bi.js?v=20260625"></script>
<script src="/js/i18n.js?v=20260624"></script> <script src="/js/i18n.js?v=20260625"></script>
<script src="/js/help.js?v=20260624"></script> <script src="/js/help.js?v=20260625"></script>
<script> <script>
'use strict'; 'use strict';
/* /*
+5 -5
View File
@@ -477,8 +477,8 @@
</main> </main>
</div> </div>
<script src="/js/api.js?v=20260624"></script> <script src="/js/api.js?v=20260625"></script>
<script src="/js/common.js?v=20260624"></script> <script src="/js/common.js?v=20260625"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js). <!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI, common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
riusando tutti gli helper di common.js (zero backend). --> riusando tutti gli helper di common.js (zero backend). -->
@@ -488,9 +488,9 @@
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
} }
</script> </script>
<script src="/js/common-bi.js?v=20260618"></script> <script src="/js/common-bi.js?v=20260625"></script>
<script src="/js/i18n.js?v=20260624"></script> <script src="/js/i18n.js?v=20260625"></script>
<script src="/js/help.js?v=20260624"></script> <script src="/js/help.js?v=20260625"></script>
<script> <script>
// ── Auth & Init ───────────────────────────────────────── // ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
+1 -1
View File
@@ -13,7 +13,7 @@
* Il nome cache include la release: va BUMPATO ad ogni rilascio (vedi version.json), * Il nome cache include la release: va BUMPATO ad ogni rilascio (vedi version.json),
* cosi' activate elimina automaticamente la shell vecchia. * cosi' activate elimina automaticamente la shell vecchia.
*/ */
const CACHE = 'nis2-shell-v1.18.3'; const CACHE = 'nis2-shell-v1.18.4';
const PRECACHE = [ const PRECACHE = [
'/offline.html', '/offline.html',
+5 -5
View File
@@ -292,8 +292,8 @@
</main> </main>
</div> </div>
<script src="/js/api.js?v=20260624"></script> <script src="/js/api.js?v=20260625"></script>
<script src="/js/common.js?v=20260624"></script> <script src="/js/common.js?v=20260625"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js). <!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI, common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
riusando tutti gli helper di common.js (zero backend). --> riusando tutti gli helper di common.js (zero backend). -->
@@ -303,9 +303,9 @@
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
} }
</script> </script>
<script src="/js/common-bi.js?v=20260618"></script> <script src="/js/common-bi.js?v=20260625"></script>
<script src="/js/i18n.js?v=20260624"></script> <script src="/js/i18n.js?v=20260625"></script>
<script src="/js/help.js?v=20260624"></script> <script src="/js/help.js?v=20260625"></script>
<script> <script>
// ── Auth & Init ───────────────────────────────────────── // ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
+1 -1
View File
@@ -1 +1 @@
{"version": "1.18.3", "build": "2026-06-16-v1.18.3", "date": "2026-06-16", "changelog": "Epic C / C3 - rimosso il modulo 'Analisi GAP ACN' (superato da C1: elenco Misure/Requisiti + valutazione per requisito). Tabelle acn_assessments/acn_assessment_responses droppate (erano vuote, 0 dati persi). RaciController ripuntato su cfg_nis2_misure (ex acn_measures.json). Sidebar/help/i18n/api/routing/tour demo ripuliti; integrazione lg231 (services/gap-analysis) intatta. Additivo-sottrattivo, nessun dato utente perso."} {"version": "1.18.4", "build": "2026-06-16-v1.18.4", "date": "2026-06-16", "changelog": "Epic C / C4 - inventario a DUE voci (ID.AM-01 Hardware, ID.AM-02 Software/Servizi/Sistemi) + sottoclassi configurabili (default di sistema + create dall'utente). mig.050: cfg_inventory_voci, inventory_subclassi, assets.voce_code/subclass_id; 20 asset migrati. AssetController voce/subclass + endpoint subclasses; assets.html selettori voce+sottoclasse. Fix: apostrofo non escapato che rompeva il JS dell'Inventario; cache-buster common-bi.js (fix sidebar C3). Additivo."}
+5 -5
View File
@@ -207,8 +207,8 @@
<!-- Report Detail Modal handled by common.js showModal --> <!-- Report Detail Modal handled by common.js showModal -->
<script src="/js/api.js?v=20260624"></script> <script src="/js/api.js?v=20260625"></script>
<script src="/js/common.js?v=20260624"></script> <script src="/js/common.js?v=20260625"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js). <!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI, common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
riusando tutti gli helper di common.js (zero backend). --> riusando tutti gli helper di common.js (zero backend). -->
@@ -218,9 +218,9 @@
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
} }
</script> </script>
<script src="/js/common-bi.js?v=20260618"></script> <script src="/js/common-bi.js?v=20260625"></script>
<script src="/js/i18n.js?v=20260624"></script> <script src="/js/i18n.js?v=20260625"></script>
<script src="/js/help.js?v=20260624"></script> <script src="/js/help.js?v=20260625"></script>
<script> <script>
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); loadSidebar();
+3 -3
View File
@@ -319,9 +319,9 @@
</main> </main>
</div> </div>
<script src="js/api.js?v=20260624"></script> <script src="js/api.js?v=20260625"></script>
<script src="js/common.js?v=20260624"></script> <script src="js/common.js?v=20260625"></script>
<script src="js/i18n.js?v=20260624"></script> <script src="js/i18n.js?v=20260625"></script>
<script> <script>
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); loadSidebar();