[FEAT] Epic C / C3 — rimosso modulo 'Analisi GAP ACN' (superato da C1)
Simon C3: l'analisi gap ACN non è un artefatto normativo e va eliminata; la conformità emerge da C1 (elenco Misure/Requisiti + valutazione per requisito in org_requisito_state). Ricognizione → migrazione → backup → rimozione (sicuro, 0 perdite): - Tabelle acn_assessments/acn_assessment_responses VUOTE (0 righe) -> droppate (mig.049, guard anti-drop se >0 righe). Ricreabili da 036. Nessun dato utente perso. - Integrazione lg231: /api/services/gap-analysis legge 'assessments' generale, NON il modulo ACN -> intatta. Verificato. - RaciController ripuntato su cfg_nis2_misure (fonte canonica) al posto di acn_measures.json (rimosso); validazione measure_code verificata (/raci/objects?type=measure -> 43 misure). Rimossi: AcnAssessmentController, public/acn-gap.html, application/data/acn_measures.json, routing acn-gap, voci sidebar (common.js + common-bi.js), metodi api.js acn*, sezione help 'acn', chiavi i18n acn.*, mapping demo-selectors. Tour demo step 3 RIPUNTATO su misure-requisiti (no step rotto, 10 step). Sidebar BI ora mostra 'Misure e Requisiti' (gap di common-bi.js sanato). Verifiche prod: /acn-gap.html 404, /api/acn-gap/* 404, /misure-requisiti 200, RaciController OK. Cache-buster ?v=20260624, version 1.18.3, SW v1.18.3. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
45e6c3599c
commit
e281aa4970
@@ -1,607 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* NIS2 Agile - Gap Analysis ACN (Determinazione 164179/2025)
|
|
||||||
* ----------------------------------------------------------------------------
|
|
||||||
* Assessment di conformita di SECONDO LIVELLO: misure e requisiti puntuali
|
|
||||||
* della Determinazione ACN n. 164179/2025 (Allegati 1 e 2), NON le 10 lettere
|
|
||||||
* generiche dell'Art. 21 NIS2 (quelle restano in AssessmentController).
|
|
||||||
*
|
|
||||||
* - soggetti IMPORTANTI (Allegato 1): 37 misure, 87 requisiti
|
|
||||||
* - soggetti ESSENZIALI (Allegato 2): 43 misure, 116 requisiti (= 87 + 29)
|
|
||||||
*
|
|
||||||
* La codifica dei requisiti e identica per i due livelli; agli essenziali si
|
|
||||||
* aggiungono misure e punti. Il perimetro applicabile e filtrato in base al
|
|
||||||
* livello del soggetto (acn_assessments.entity_level, derivato da
|
|
||||||
* organizations.entity_type).
|
|
||||||
*
|
|
||||||
* Fonte testi: application/data/acn_measures.json (estratto integrale ACN).
|
|
||||||
*
|
|
||||||
* Endpoint (base /api/acn-gap):
|
|
||||||
* GET /catalog - catalogo misure/requisiti per il livello dell'org
|
|
||||||
* GET /list - assessment ACN dell'org
|
|
||||||
* POST /create - crea assessment (pre-popola requisiti applicabili)
|
|
||||||
* GET /{id} - intestazione + stats
|
|
||||||
* GET /{id}/requirements - requisiti raggruppati per funzione/categoria/misura
|
|
||||||
* POST /{id}/respond - salva risposta (singola o batch)
|
|
||||||
* POST /{id}/complete - calcola score e completa
|
|
||||||
* GET /{id}/report - report completo
|
|
||||||
* POST /{id}/aiAnalyze - analisi AI con grounding sui requisiti ACN
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/BaseController.php';
|
|
||||||
|
|
||||||
class AcnAssessmentController extends BaseController
|
|
||||||
{
|
|
||||||
/** Cache in-process del dataset misure. */
|
|
||||||
private ?array $measuresCache = null;
|
|
||||||
|
|
||||||
// ════════════════════════ CATALOGO ════════════════════════
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/acn-gap/catalog
|
|
||||||
* Catalogo dei requisiti ACN applicabili al livello del soggetto corrente
|
|
||||||
* (importante/essenziale), raggruppato per funzione FW. Sola lettura,
|
|
||||||
* non crea assessment. Utile per anteprima/consultazione normativa.
|
|
||||||
*/
|
|
||||||
public function catalog(): void
|
|
||||||
{
|
|
||||||
$this->requireOrgAccess();
|
|
||||||
$level = $this->resolveEntityLevel();
|
|
||||||
if ($level === null) {
|
|
||||||
// Coerente con create(): senza classificazione non c'e perimetro.
|
|
||||||
// Il frontend intercetta ENTITY_LEVEL_REQUIRED e mostra l'invito a classificare.
|
|
||||||
$this->jsonError(
|
|
||||||
'Il soggetto non e classificato come importante o essenziale. Completa prima la classificazione NIS2 dell\'organizzazione.',
|
|
||||||
422,
|
|
||||||
'ENTITY_LEVEL_REQUIRED'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$grouped = $this->groupedRequirements($level, []);
|
|
||||||
|
|
||||||
$totals = $this->datasetTotals();
|
|
||||||
$this->jsonSuccess([
|
|
||||||
'entity_level' => $level,
|
|
||||||
'totals' => $totals[$level],
|
|
||||||
'functions' => $grouped,
|
|
||||||
'source' => $this->measures()['source'] ?? null,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ════════════════════════ LISTA / CREATE ════════════════════════
|
|
||||||
|
|
||||||
/** GET /api/acn-gap/list */
|
|
||||||
public function list(): void
|
|
||||||
{
|
|
||||||
$this->requireOrgAccess();
|
|
||||||
$orgId = $this->getCurrentOrgId();
|
|
||||||
$rows = Database::fetchAll(
|
|
||||||
'SELECT id, title, entity_level, status, overall_score, completed_at, created_at
|
|
||||||
FROM acn_assessments
|
|
||||||
WHERE organization_id = ?
|
|
||||||
ORDER BY created_at DESC',
|
|
||||||
[$orgId]
|
|
||||||
);
|
|
||||||
$this->jsonSuccess(['assessments' => $rows]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/acn-gap/create Body: { title? }
|
|
||||||
* Crea un assessment ACN e pre-popola TUTTI i requisiti applicabili al
|
|
||||||
* livello del soggetto (snapshot del testo congelato).
|
|
||||||
*/
|
|
||||||
public function create(): void
|
|
||||||
{
|
|
||||||
$this->requireOrgRole(['org_admin', 'compliance_manager']);
|
|
||||||
$orgId = $this->getCurrentOrgId();
|
|
||||||
$userId = $this->getCurrentUserId();
|
|
||||||
$level = $this->resolveEntityLevel();
|
|
||||||
|
|
||||||
if ($level === null) {
|
|
||||||
$this->jsonError(
|
|
||||||
'Il soggetto non e classificato come importante o essenziale. Completa prima la classificazione NIS2 dell\'organizzazione.',
|
|
||||||
422,
|
|
||||||
'ENTITY_LEVEL_REQUIRED'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$title = trim((string) $this->getParam('title', ''));
|
|
||||||
if ($title === '') {
|
|
||||||
$title = 'Gap Analysis ACN ' . date('Y-m-d');
|
|
||||||
}
|
|
||||||
|
|
||||||
Database::beginTransaction();
|
|
||||||
try {
|
|
||||||
$assessmentId = Database::insert('acn_assessments', [
|
|
||||||
'organization_id' => $orgId,
|
|
||||||
'title' => $title,
|
|
||||||
'entity_level' => $level,
|
|
||||||
'status' => 'draft',
|
|
||||||
'created_by' => $userId,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$applicable = $this->applicableRequirements($level);
|
|
||||||
foreach ($applicable as $r) {
|
|
||||||
Database::insert('acn_assessment_responses', [
|
|
||||||
'assessment_id' => $assessmentId,
|
|
||||||
'organization_id' => $orgId,
|
|
||||||
'requirement_key' => $r['key'],
|
|
||||||
'measure_code' => $r['measure_code'],
|
|
||||||
'requirement_index' => $r['index'],
|
|
||||||
'function_code' => $r['function_code'],
|
|
||||||
'category_code' => $r['category_code'],
|
|
||||||
'requirement_text' => $r['text'],
|
|
||||||
'response_value' => null,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
Database::commit();
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
if (Database::getInstance()->inTransaction()) {
|
|
||||||
Database::rollback();
|
|
||||||
}
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->logAudit('acn_assessment_created', 'acn_assessment', $assessmentId, [
|
|
||||||
'entity_level' => $level,
|
|
||||||
'requirements' => count($applicable),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->jsonSuccess([
|
|
||||||
'id' => $assessmentId,
|
|
||||||
'entity_level' => $level,
|
|
||||||
'requirements' => count($applicable),
|
|
||||||
], 'Assessment ACN creato', 201);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ════════════════════════ DETTAGLIO ════════════════════════
|
|
||||||
|
|
||||||
/** GET /api/acn-gap/{id} */
|
|
||||||
public function get(int $id): void
|
|
||||||
{
|
|
||||||
$this->requireOrgAccess();
|
|
||||||
$a = $this->loadOwned($id);
|
|
||||||
$a['stats'] = $this->computeStats($id);
|
|
||||||
$this->jsonSuccess($a);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/acn-gap/{id}/requirements
|
|
||||||
* Requisiti raggruppati per funzione -> categoria -> misura, con la risposta
|
|
||||||
* salvata. Struttura pensata per il rendering a wizard/accordion.
|
|
||||||
*/
|
|
||||||
public function getRequirements(int $id): void
|
|
||||||
{
|
|
||||||
$this->requireOrgAccess();
|
|
||||||
$a = $this->loadOwned($id);
|
|
||||||
|
|
||||||
$rows = Database::fetchAll(
|
|
||||||
'SELECT requirement_key, measure_code, requirement_index, function_code,
|
|
||||||
category_code, requirement_text, response_value, evidence_description, notes, answered_at
|
|
||||||
FROM acn_assessment_responses
|
|
||||||
WHERE assessment_id = ?
|
|
||||||
ORDER BY id',
|
|
||||||
[$id]
|
|
||||||
);
|
|
||||||
|
|
||||||
// arricchimento da dataset: titoli misura/categoria/funzione
|
|
||||||
$meta = $this->measureMeta();
|
|
||||||
$byFunc = [];
|
|
||||||
foreach ($rows as $r) {
|
|
||||||
$fc = $r['function_code'];
|
|
||||||
$cc = $r['category_code'];
|
|
||||||
$mc = $r['measure_code'];
|
|
||||||
$byFunc[$fc] ??= [
|
|
||||||
'function_code' => $fc,
|
|
||||||
'function_name' => $meta['functions'][$fc] ?? $fc,
|
|
||||||
'categories' => [],
|
|
||||||
];
|
|
||||||
$byFunc[$fc]['categories'][$cc] ??= [
|
|
||||||
'category_code' => $cc,
|
|
||||||
'category_title' => $meta['categories'][$cc] ?? $cc,
|
|
||||||
'measures' => [],
|
|
||||||
];
|
|
||||||
$byFunc[$fc]['categories'][$cc]['measures'][$mc] ??= [
|
|
||||||
'measure_code' => $mc,
|
|
||||||
'measure_title' => $meta['measures'][$mc] ?? $mc,
|
|
||||||
'requirements' => [],
|
|
||||||
];
|
|
||||||
$byFunc[$fc]['categories'][$cc]['measures'][$mc]['requirements'][] = [
|
|
||||||
'key' => $r['requirement_key'],
|
|
||||||
'index' => (int) $r['requirement_index'],
|
|
||||||
'text' => $r['requirement_text'],
|
|
||||||
'response' => $r['response_value'],
|
|
||||||
'evidence' => $r['evidence_description'],
|
|
||||||
'notes' => $r['notes'],
|
|
||||||
'answered_at' => $r['answered_at'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// converti mappe associative in liste ordinate
|
|
||||||
$functions = [];
|
|
||||||
foreach ($byFunc as $f) {
|
|
||||||
$cats = [];
|
|
||||||
foreach ($f['categories'] as $c) {
|
|
||||||
$c['measures'] = array_values($c['measures']);
|
|
||||||
$cats[] = $c;
|
|
||||||
}
|
|
||||||
$f['categories'] = $cats;
|
|
||||||
$functions[] = $f;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->jsonSuccess([
|
|
||||||
'assessment' => ['id' => $id, 'entity_level' => $a['entity_level'], 'status' => $a['status']],
|
|
||||||
'functions' => $functions,
|
|
||||||
'stats' => $this->computeStats($id),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/acn-gap/{id}/respond
|
|
||||||
* Body: { requirement_key, response_value, evidence?, notes? }
|
|
||||||
* oppure { responses: [ {requirement_key, response_value, ...}, ... ] }
|
|
||||||
*/
|
|
||||||
public function saveResponse(int $id): void
|
|
||||||
{
|
|
||||||
$this->requireOrgRole(['org_admin', 'compliance_manager', 'auditor']);
|
|
||||||
$a = $this->loadOwned($id);
|
|
||||||
if ($a['status'] === 'completed') {
|
|
||||||
$this->jsonError('Assessment gia completato: non modificabile.', 409, 'ALREADY_COMPLETED');
|
|
||||||
}
|
|
||||||
$userId = $this->getCurrentUserId();
|
|
||||||
$orgId = $this->getCurrentOrgId();
|
|
||||||
|
|
||||||
$body = $this->getJsonBody();
|
|
||||||
$batch = is_array($body['responses'] ?? null) ? $body['responses'] : [$body];
|
|
||||||
|
|
||||||
$valid = ['not_implemented', 'partial', 'implemented', 'not_applicable'];
|
|
||||||
$saved = 0;
|
|
||||||
foreach ($batch as $item) {
|
|
||||||
if (!is_array($item)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$key = trim((string) ($item['requirement_key'] ?? ''));
|
|
||||||
$val = (string) ($item['response_value'] ?? '');
|
|
||||||
if ($key === '' || !in_array($val, $valid, true)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$updated = Database::update('acn_assessment_responses', [
|
|
||||||
'response_value' => $val,
|
|
||||||
'evidence_description' => isset($item['evidence']) ? (string) $item['evidence'] : null,
|
|
||||||
'notes' => isset($item['notes']) ? (string) $item['notes'] : null,
|
|
||||||
'answered_by' => $userId,
|
|
||||||
'answered_at' => date('Y-m-d H:i:s'),
|
|
||||||
], 'assessment_id = ? AND requirement_key = ? AND organization_id = ?', [$id, $key, $orgId]);
|
|
||||||
$saved += $updated > 0 ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// porta a in_progress se era draft
|
|
||||||
if ($a['status'] === 'draft' && $saved > 0) {
|
|
||||||
Database::update('acn_assessments', ['status' => 'in_progress'], 'id = ?', [$id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->jsonSuccess(['saved' => $saved, 'stats' => $this->computeStats($id)], 'Risposte salvate');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/acn-gap/{id}/complete
|
|
||||||
* Calcola overall_score + function_scores + stats e marca completed.
|
|
||||||
* Richiede che tutti i requisiti applicabili abbiano una risposta.
|
|
||||||
*/
|
|
||||||
public function complete(int $id): void
|
|
||||||
{
|
|
||||||
$this->requireOrgRole(['org_admin', 'compliance_manager']);
|
|
||||||
$a = $this->loadOwned($id);
|
|
||||||
if ($a['status'] === 'completed') {
|
|
||||||
$this->jsonError('Assessment gia completato.', 409, 'ALREADY_COMPLETED');
|
|
||||||
}
|
|
||||||
|
|
||||||
$unanswered = (int) (Database::fetchOne(
|
|
||||||
'SELECT COUNT(*) AS n FROM acn_assessment_responses WHERE assessment_id = ? AND response_value IS NULL',
|
|
||||||
[$id]
|
|
||||||
)['n'] ?? 0);
|
|
||||||
if ($unanswered > 0) {
|
|
||||||
$this->jsonError(
|
|
||||||
"Mancano $unanswered requisiti senza risposta. Completa tutte le risposte prima di chiudere.",
|
|
||||||
422,
|
|
||||||
'INCOMPLETE',
|
|
||||||
['unanswered' => $unanswered]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[$overall, $funcScores, $stats] = $this->calculateScores($id);
|
|
||||||
|
|
||||||
Database::update('acn_assessments', [
|
|
||||||
'status' => 'completed',
|
|
||||||
'overall_score' => $overall,
|
|
||||||
'function_scores' => json_encode($funcScores, JSON_UNESCAPED_UNICODE),
|
|
||||||
'stats' => json_encode($stats, JSON_UNESCAPED_UNICODE),
|
|
||||||
'completed_by' => $this->getCurrentUserId(),
|
|
||||||
'completed_at' => date('Y-m-d H:i:s'),
|
|
||||||
], 'id = ?', [$id]);
|
|
||||||
|
|
||||||
$this->logAudit('acn_assessment_completed', 'acn_assessment', $id, [
|
|
||||||
'overall_score' => $overall,
|
|
||||||
'entity_level' => $a['entity_level'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->jsonSuccess([
|
|
||||||
'overall_score' => $overall,
|
|
||||||
'function_scores' => $funcScores,
|
|
||||||
'stats' => $stats,
|
|
||||||
], 'Assessment ACN completato');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** GET /api/acn-gap/{id}/report */
|
|
||||||
public function getReport(int $id): void
|
|
||||||
{
|
|
||||||
$this->requireOrgAccess();
|
|
||||||
$a = $this->loadOwned($id);
|
|
||||||
|
|
||||||
// requisiti non conformi (priorita per il piano d'azione)
|
|
||||||
$gaps = Database::fetchAll(
|
|
||||||
"SELECT requirement_key, measure_code, function_code, category_code, requirement_text,
|
|
||||||
response_value, notes
|
|
||||||
FROM acn_assessment_responses
|
|
||||||
WHERE assessment_id = ? AND response_value IN ('not_implemented','partial')
|
|
||||||
ORDER BY FIELD(response_value,'not_implemented','partial'), function_code, measure_code, requirement_index",
|
|
||||||
[$id]
|
|
||||||
);
|
|
||||||
|
|
||||||
$a['function_scores'] = $a['function_scores'] ? json_decode($a['function_scores'], true) : null;
|
|
||||||
$a['stats'] = $a['stats'] ? json_decode($a['stats'], true) : $this->computeStats($id);
|
|
||||||
$a['ai_recommendations'] = $a['ai_recommendations'] ? json_decode($a['ai_recommendations'], true) : null;
|
|
||||||
|
|
||||||
$this->jsonSuccess(['assessment' => $a, 'gaps' => $gaps]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/acn-gap/{id}/aiAnalyze
|
|
||||||
* Analisi AI dei gap con grounding sui requisiti ACN (la KB contiene gia i
|
|
||||||
* 203 requisiti, scope SYSTEM). Salva sintesi e raccomandazioni.
|
|
||||||
*/
|
|
||||||
public function aiAnalyze(int $id): void
|
|
||||||
{
|
|
||||||
$this->requireOrgRole(['org_admin', 'compliance_manager']);
|
|
||||||
$a = $this->loadOwned($id);
|
|
||||||
|
|
||||||
$gaps = Database::fetchAll(
|
|
||||||
"SELECT measure_code, requirement_index, requirement_text, response_value
|
|
||||||
FROM acn_assessment_responses
|
|
||||||
WHERE assessment_id = ? AND response_value IN ('not_implemented','partial')
|
|
||||||
ORDER BY function_code, measure_code, requirement_index
|
|
||||||
LIMIT 60",
|
|
||||||
[$id]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (empty($gaps)) {
|
|
||||||
$this->jsonSuccess(['ai_summary' => 'Nessun gap rilevato: tutti i requisiti applicabili risultano conformi.'], 'Analisi completata');
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once APP_PATH . '/services/AIService.php';
|
|
||||||
$ai = new AIService();
|
|
||||||
|
|
||||||
$lines = array_map(
|
|
||||||
fn($g) => "- {$g['measure_code']} punto {$g['requirement_index']} [{$g['response_value']}]: {$g['requirement_text']}",
|
|
||||||
$gaps
|
|
||||||
);
|
|
||||||
$question = "Analizza i seguenti gap di conformita rispetto alle misure di sicurezza di base ACN "
|
|
||||||
. "(Determinazione 164179/2025) per un soggetto " . ($a['entity_level'] === 'essential' ? 'ESSENZIALE' : 'IMPORTANTE') . ". "
|
|
||||||
. "Per ciascun gap suggerisci un'azione concreta e prioritizza. Cita SOLO requisiti ACN reali.\n\n"
|
|
||||||
. implode("\n", $lines);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$userContext = [
|
|
||||||
'organization_id' => $this->getCurrentOrgId(),
|
|
||||||
'consulting_firm_id' => $this->currentUser['consulting_firm_id'] ?? null,
|
|
||||||
];
|
|
||||||
// askWithRag ritorna ['answer'=>string, 'sources'=>array, 'rag_used'=>bool]:
|
|
||||||
// estraiamo il testo, NON salviamo l'intero array.
|
|
||||||
$result = $ai->askWithRag($question, $userContext);
|
|
||||||
$answer = is_array($result) ? (string) ($result['answer'] ?? '') : (string) $result;
|
|
||||||
$sources = (is_array($result) && isset($result['sources'])) ? $result['sources'] : [];
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
error_log('[AcnAssessment] aiAnalyze fallita: ' . $e->getMessage());
|
|
||||||
$this->jsonError('Analisi AI temporaneamente non disponibile. Riprova piu tardi.', 503, 'AI_UNAVAILABLE');
|
|
||||||
}
|
|
||||||
|
|
||||||
Database::update('acn_assessments', [
|
|
||||||
'ai_summary' => $answer,
|
|
||||||
'ai_recommendations' => json_encode($sources, JSON_UNESCAPED_UNICODE),
|
|
||||||
], 'id = ?', [$id]);
|
|
||||||
|
|
||||||
$this->logAudit('acn_assessment_ai_analyzed', 'acn_assessment', $id, ['gaps' => count($gaps)]);
|
|
||||||
$this->jsonSuccess(['ai_summary' => $answer, 'sources' => $sources], 'Analisi AI completata');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ════════════════════════ HELPER ════════════════════════
|
|
||||||
|
|
||||||
/** Carica l'assessment verificando ownership org (anti-IDOR). */
|
|
||||||
private function loadOwned(int $id): array
|
|
||||||
{
|
|
||||||
$a = Database::fetchOne(
|
|
||||||
'SELECT * FROM acn_assessments WHERE id = ? AND organization_id = ?',
|
|
||||||
[$id, $this->getCurrentOrgId()]
|
|
||||||
);
|
|
||||||
if (!$a) {
|
|
||||||
$this->jsonError('Assessment non trovato', 404, 'NOT_FOUND');
|
|
||||||
}
|
|
||||||
return $a;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determina il livello del soggetto dall'organization corrente.
|
|
||||||
* organizations.entity_type: 'essential'|'important'|'not_applicable'.
|
|
||||||
* Ritorna 'essential'|'important', oppure null se non classificato.
|
|
||||||
*/
|
|
||||||
private function resolveEntityLevel(): ?string
|
|
||||||
{
|
|
||||||
$org = Database::fetchOne('SELECT entity_type FROM organizations WHERE id = ?', [$this->getCurrentOrgId()]);
|
|
||||||
$t = $org['entity_type'] ?? null;
|
|
||||||
if ($t === 'essential' || $t === 'important') {
|
|
||||||
return $t;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Carica e cache il dataset misure ACN. */
|
|
||||||
private function measures(): array
|
|
||||||
{
|
|
||||||
if ($this->measuresCache === null) {
|
|
||||||
$path = APP_PATH . '/data/acn_measures.json';
|
|
||||||
$json = is_readable($path) ? json_decode((string) file_get_contents($path), true) : null;
|
|
||||||
$this->measuresCache = is_array($json) ? $json : ['measures' => []];
|
|
||||||
}
|
|
||||||
return $this->measuresCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Totali requisiti per livello (da dataset). */
|
|
||||||
private function datasetTotals(): array
|
|
||||||
{
|
|
||||||
$t = $this->measures()['totals'] ?? [];
|
|
||||||
return [
|
|
||||||
'important' => [
|
|
||||||
'measures' => $t['measures_importante'] ?? 37,
|
|
||||||
'requirements' => $t['requirements_importante'] ?? 87,
|
|
||||||
],
|
|
||||||
'essential' => [
|
|
||||||
'measures' => $t['measures_essenziale'] ?? 43,
|
|
||||||
'requirements' => $t['requirements_essenziale'] ?? 116,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lista piatta dei requisiti applicabili a un livello.
|
|
||||||
* @return array<int,array{key:string,measure_code:string,index:int,function_code:string,category_code:string,text:string}>
|
|
||||||
*/
|
|
||||||
private function applicableRequirements(string $level): array
|
|
||||||
{
|
|
||||||
$flag = $level === 'essential' ? 'essenziale' : 'importante';
|
|
||||||
$out = [];
|
|
||||||
foreach ($this->measures()['measures'] ?? [] as $m) {
|
|
||||||
$applies = $level === 'essential' ? ($m['applies_essenziale'] ?? false) : ($m['applies_importante'] ?? false);
|
|
||||||
if (!$applies) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
foreach ($m['requirements'] ?? [] as $r) {
|
|
||||||
if (empty($r[$flag])) {
|
|
||||||
continue; // punto non applicabile a questo livello
|
|
||||||
}
|
|
||||||
$out[] = [
|
|
||||||
'key' => $m['code'] . '#' . $r['index'],
|
|
||||||
'measure_code' => $m['code'],
|
|
||||||
'index' => (int) $r['index'],
|
|
||||||
'function_code' => $m['function_code'],
|
|
||||||
'category_code' => $m['category_code'],
|
|
||||||
'text' => $r['text'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Requisiti raggruppati per funzione (per il catalogo). */
|
|
||||||
private function groupedRequirements(?string $level, array $responses): array
|
|
||||||
{
|
|
||||||
if ($level === null) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
$reqs = $this->applicableRequirements($level);
|
|
||||||
$meta = $this->measureMeta();
|
|
||||||
$byFunc = [];
|
|
||||||
foreach ($reqs as $r) {
|
|
||||||
$fc = $r['function_code'];
|
|
||||||
$byFunc[$fc] ??= ['function_code' => $fc, 'function_name' => $meta['functions'][$fc] ?? $fc, 'measures' => []];
|
|
||||||
$mc = $r['measure_code'];
|
|
||||||
$byFunc[$fc]['measures'][$mc] ??= ['measure_code' => $mc, 'measure_title' => $meta['measures'][$mc] ?? $mc, 'requirements' => []];
|
|
||||||
$byFunc[$fc]['measures'][$mc]['requirements'][] = ['index' => $r['index'], 'text' => $r['text']];
|
|
||||||
}
|
|
||||||
$out = [];
|
|
||||||
foreach ($byFunc as $f) {
|
|
||||||
$f['measures'] = array_values($f['measures']);
|
|
||||||
$out[] = $f;
|
|
||||||
}
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Mappe codice->titolo per funzioni/categorie/misure (dal dataset). */
|
|
||||||
private function measureMeta(): array
|
|
||||||
{
|
|
||||||
static $meta = null;
|
|
||||||
if ($meta !== null) {
|
|
||||||
return $meta;
|
|
||||||
}
|
|
||||||
$functions = [];
|
|
||||||
$categories = [];
|
|
||||||
$measures = [];
|
|
||||||
foreach ($this->measures()['measures'] ?? [] as $m) {
|
|
||||||
$functions[$m['function_code']] = $m['function_name'] ?? $m['function_code'];
|
|
||||||
$categories[$m['category_code']] = $m['category_title'] ?? $m['category_code'];
|
|
||||||
$measures[$m['code']] = $m['title'] ?? $m['code'];
|
|
||||||
}
|
|
||||||
$meta = ['functions' => $functions, 'categories' => $categories, 'measures' => $measures];
|
|
||||||
return $meta;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Conteggi rapidi per stato risposta. */
|
|
||||||
private function computeStats(int $id): array
|
|
||||||
{
|
|
||||||
$rows = Database::fetchAll(
|
|
||||||
'SELECT response_value, COUNT(*) AS n FROM acn_assessment_responses WHERE assessment_id = ? GROUP BY response_value',
|
|
||||||
[$id]
|
|
||||||
);
|
|
||||||
$stats = ['applicable' => 0, 'implemented' => 0, 'partial' => 0, 'not_implemented' => 0, 'not_applicable' => 0, 'unanswered' => 0];
|
|
||||||
foreach ($rows as $r) {
|
|
||||||
$n = (int) $r['n'];
|
|
||||||
$stats['applicable'] += $n;
|
|
||||||
$v = $r['response_value'];
|
|
||||||
if ($v === null) {
|
|
||||||
$stats['unanswered'] += $n;
|
|
||||||
} elseif (isset($stats[$v])) {
|
|
||||||
$stats[$v] += $n;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calcolo punteggi: overall + per funzione.
|
|
||||||
* Scoring per requisito: implemented=100, partial=50, not_implemented=0.
|
|
||||||
* not_applicable e unanswered ESCLUSI dal denominatore (coerente con
|
|
||||||
* l'assessment Art.21 esistente).
|
|
||||||
* @return array{0:float,1:array,2:array}
|
|
||||||
*/
|
|
||||||
private function calculateScores(int $id): array
|
|
||||||
{
|
|
||||||
$rows = Database::fetchAll(
|
|
||||||
'SELECT function_code, response_value FROM acn_assessment_responses WHERE assessment_id = ?',
|
|
||||||
[$id]
|
|
||||||
);
|
|
||||||
$val = ['implemented' => 100.0, 'partial' => 50.0, 'not_implemented' => 0.0];
|
|
||||||
|
|
||||||
$sum = 0.0; $cnt = 0;
|
|
||||||
$byFunc = [];
|
|
||||||
foreach ($rows as $r) {
|
|
||||||
$v = $r['response_value'];
|
|
||||||
if ($v === 'not_applicable' || $v === null || !isset($val[$v])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$fc = $r['function_code'];
|
|
||||||
$byFunc[$fc] ??= ['sum' => 0.0, 'cnt' => 0];
|
|
||||||
$byFunc[$fc]['sum'] += $val[$v];
|
|
||||||
$byFunc[$fc]['cnt']++;
|
|
||||||
$sum += $val[$v];
|
|
||||||
$cnt++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Se nessun requisito e applicabile (tutti not_applicable) il punteggio
|
|
||||||
// non e definito: null, non 0.0 (che significherebbe "0% conforme").
|
|
||||||
$overall = $cnt > 0 ? round($sum / $cnt, 2) : null;
|
|
||||||
$funcScores = [];
|
|
||||||
foreach ($byFunc as $fc => $d) {
|
|
||||||
$funcScores[$fc] = $d['cnt'] > 0 ? round($d['sum'] / $d['cnt'], 2) : 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [$overall, $funcScores, $this->computeStats($id)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,8 +22,8 @@
|
|||||||
* - 'inventario' = `assets`; 'procedure' = `policies`; rischi = `risks`;
|
* - 'inventario' = `assets`; 'procedure' = `policies`; rischi = `risks`;
|
||||||
* fornitori = `suppliers`. Label: assets.name, policies.title, risks.title,
|
* fornitori = `suppliers`. Label: assets.name, policies.title, risks.title,
|
||||||
* suppliers.name, org_roles.role_name.
|
* suppliers.name, org_roles.role_name.
|
||||||
* - misure ACN = application/data/acn_measures.json (campo 'code'); NON e una
|
* - misure = tabella canonica cfg_nis2_misure (campo misura_code); risk_measure.measure_code
|
||||||
* tabella DB -> risk_measure.measure_code e una stringa validata sul JSON.
|
* e una stringa validata su quella tabella (Epic C / C1; ex acn_measures.json, rimosso con C3).
|
||||||
* - raci_assignments.object_id e POLIMORFICO -> niente FK; esistenza verificata
|
* - raci_assignments.object_id e POLIMORFICO -> niente FK; esistenza verificata
|
||||||
* in PHP. I nomi tabella/colonna provengono SEMPRE dalle const-map qui sotto
|
* in PHP. I nomi tabella/colonna provengono SEMPRE dalle const-map qui sotto
|
||||||
* dopo validazione enum (mai da stringhe grezze della request).
|
* dopo validazione enum (mai da stringhe grezze della request).
|
||||||
@@ -461,15 +461,18 @@ class RaciController extends BaseController
|
|||||||
return $lt;
|
return $lt;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Carica e memoizza il catalogo misure ACN dal JSON. */
|
/**
|
||||||
|
* Carica e memoizza il catalogo misure dalla tabella canonica cfg_nis2_misure
|
||||||
|
* (Epic C / C1). Forma di ritorno invariata (['code','title']) per compatibilità
|
||||||
|
* con assertMeasureExists/measureLabel. Sostituisce il vecchio acn_measures.json.
|
||||||
|
*/
|
||||||
private function loadMeasures(): array
|
private function loadMeasures(): array
|
||||||
{
|
{
|
||||||
if (self::$measuresCache === null) {
|
if (self::$measuresCache === null) {
|
||||||
$path = APP_PATH . '/data/acn_measures.json';
|
$rows = Database::fetchAll('SELECT misura_code, misura_descr FROM cfg_nis2_misure ORDER BY ord ASC');
|
||||||
$raw = is_readable($path) ? file_get_contents($path) : false;
|
self::$measuresCache = array_map(static function ($r) {
|
||||||
$data = $raw !== false ? json_decode($raw, true) : null;
|
return ['code' => $r['misura_code'], 'title' => $r['misura_descr']];
|
||||||
self::$measuresCache = is_array($data) && isset($data['measures']) && is_array($data['measures'])
|
}, $rows);
|
||||||
? $data['measures'] : [];
|
|
||||||
}
|
}
|
||||||
return self::$measuresCache;
|
return self::$measuresCache;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
|||||||
|
-- =====================================================================
|
||||||
|
-- 049 — Rimozione modulo "Analisi GAP ACN" (Epic C / C3)
|
||||||
|
-- =====================================================================
|
||||||
|
-- Il modulo è superato da C1 (elenco Misure/Requisiti canonico + valutazione
|
||||||
|
-- per requisito in org_requisito_state). Tabelle VUOTE (0 righe verificate),
|
||||||
|
-- nessun dato utente perso. La funzione di conformità misura-per-misura vive
|
||||||
|
-- ora in org_requisito_state (mig.047).
|
||||||
|
-- Reversibile: ricreabili da docs/sql/036_acn_gap_assessment.sql.
|
||||||
|
-- =====================================================================
|
||||||
|
DROP TABLE IF EXISTS acn_assessment_responses;
|
||||||
|
DROP TABLE IF EXISTS acn_assessments;
|
||||||
@@ -70,8 +70,8 @@
|
|||||||
</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=20260623"></script>
|
<script src="js/api.js?v=20260624"></script>
|
||||||
<script src="js/common.js?v=20260623"></script>
|
<script src="js/common.js?v=20260624"></script>
|
||||||
<script src="js/common-bi.js?v=20260618"></script>
|
<script src="js/common-bi.js?v=20260618"></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)
|
||||||
|
|||||||
@@ -1,451 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="it">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
||||||
<title>Gap Analysis ACN - 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 (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?v=20260613b">
|
|
||||||
<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; }
|
|
||||||
.acn-level-essential { background:#fee2e2; color:#991b1b; }
|
|
||||||
.acn-func { border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; margin-bottom:14px; overflow:hidden; }
|
|
||||||
.acn-func-head { background:var(--gray-50,#f9fafb); padding:12px 16px; cursor:pointer; display:flex; justify-content:space-between; align-items:center; gap:12px; }
|
|
||||||
.acn-func-head strong { font-size:1rem; }
|
|
||||||
.acn-func-meta { font-size:.8rem; color:var(--gray-500,#6b7280); }
|
|
||||||
.acn-func-body { padding:8px 16px 16px; display:none; }
|
|
||||||
.acn-func.open .acn-func-body { display:block; }
|
|
||||||
.acn-measure { border-top:1px solid var(--gray-100,#f3f4f6); padding:14px 0; }
|
|
||||||
.acn-measure-code { display:inline-block; background:#eef2ff; color:#3730a3; font-weight:700; font-size:.72rem; padding:2px 8px; border-radius:6px; margin-right:8px; }
|
|
||||||
.acn-measure-title { font-weight:600; font-size:.92rem; }
|
|
||||||
.acn-req { padding:12px 0 12px 14px; border-left:2px solid var(--gray-100,#f3f4f6); margin:10px 0 10px 8px; }
|
|
||||||
.acn-req-text { font-size:.9rem; margin-bottom:8px; }
|
|
||||||
.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; }
|
|
||||||
.acn-opt.sel-not_applicable { background:#6b7280; color:#fff; border-color:#6b7280; }
|
|
||||||
.acn-progress { position:sticky; top:0; background:#fff; z-index:5; padding:12px 0; border-bottom:1px solid var(--gray-100,#f3f4f6); }
|
|
||||||
.acn-bar { height:8px; background:var(--gray-100,#f3f4f6); border-radius:4px; overflow:hidden; margin:8px 0; }
|
|
||||||
.acn-bar-fill { height:100%; background:var(--primary,#2563eb); width:0; transition:width .3s; }
|
|
||||||
.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>
|
|
||||||
<!-- 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">
|
|
||||||
<header class="content-header">
|
|
||||||
<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">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="content-body">
|
|
||||||
<div class="acn-intro" id="acn-intro">
|
|
||||||
<strong data-i18n="acn.intro.title">Conformità puntuale alla Determinazione ACN 164179/2025.</strong>
|
|
||||||
<span data-i18n="acn.intro.body">Questa analisi va oltre le 10 misure generiche dell'Art. 21: valuta le misure e i requisiti puntuali stabiliti dall'ACN per i soggetti NIS. Il perimetro dipende dalla classificazione del soggetto (importante o essenziale).</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- START -->
|
|
||||||
<div id="acn-start" class="card mb-24">
|
|
||||||
<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">
|
|
||||||
<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">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- CATALOG (read-only) -->
|
|
||||||
<div id="acn-catalog" class="card mb-24" style="display:none;">
|
|
||||||
<div class="card-header"><h3 data-i18n="acn.catalog.title">Catalogo requisiti ACN applicabili</h3></div>
|
|
||||||
<div class="card-body" id="acn-catalog-body"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- WIZARD -->
|
|
||||||
<div id="acn-wizard" style="display:none;">
|
|
||||||
<div class="card acn-progress">
|
|
||||||
<div class="card-body" style="padding:14px 18px;">
|
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap;">
|
|
||||||
<strong id="acn-wizard-title">Gap Analysis ACN</strong>
|
|
||||||
<span id="acn-wizard-level"></span>
|
|
||||||
</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" role="status" aria-live="polite"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card mb-24">
|
|
||||||
<div class="card-body" id="acn-functions"></div>
|
|
||||||
</div>
|
|
||||||
<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">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- RESULTS -->
|
|
||||||
<div id="acn-results" style="display:none;">
|
|
||||||
<div class="card mb-24">
|
|
||||||
<div class="card-header"><h3 data-i18n="acn.results.title">Esito Gap Analysis ACN</h3></div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div style="text-align:center; margin-bottom:20px;">
|
|
||||||
<div style="font-size:3rem; font-weight:800;" id="acn-overall">0</div>
|
|
||||||
<div class="text-muted" data-i18n="acn.results.overall">Punteggio complessivo di conformità (0-100)</div>
|
|
||||||
</div>
|
|
||||||
<h4 style="margin:16px 0 8px;" data-i18n="acn.results.byfunc">Punteggio per funzione (Framework Nazionale)</h4>
|
|
||||||
<div id="acn-func-scores"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card mb-24" id="acn-ai-card" style="display:none;">
|
|
||||||
<div class="card-header"><h3 data-i18n="acn.results.ai">Analisi AI dei gap</h3></div>
|
|
||||||
<div class="card-body" id="acn-ai-body"></div>
|
|
||||||
</div>
|
|
||||||
<div class="card mb-24">
|
|
||||||
<div class="card-header"><h3 data-i18n="acn.results.gaps">Requisiti non conformi (piano d'azione)</h3></div>
|
|
||||||
<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">
|
|
||||||
<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/api.js?v=20260623"></script>
|
|
||||||
<script src="/js/common.js?v=20260623"></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=20260618"></script>
|
|
||||||
<script src="/js/i18n.js?v=20260623"></script>
|
|
||||||
<script src="/js/help.js?v=20260623"></script>
|
|
||||||
<script>
|
|
||||||
'use strict';
|
|
||||||
const FUNC_LABELS = {
|
|
||||||
GOVERN: { it:'Governo (GV)', en:'Govern (GV)' },
|
|
||||||
IDENTIFY: { it:'Identificazione (ID)', en:'Identify (ID)' },
|
|
||||||
PROTECT: { it:'Protezione (PR)', en:'Protect (PR)' },
|
|
||||||
DETECT: { it:'Rilevamento (DE)', en:'Detect (DE)' },
|
|
||||||
RESPOND: { it:'Risposta (RS)', en:'Respond (RS)' },
|
|
||||||
RECOVER: { it:'Ripristino (RC)', en:'Recover (RC)' }
|
|
||||||
};
|
|
||||||
let ACN = { id:null, level:null, answers:{}, total:0 };
|
|
||||||
let saveTimer = null;
|
|
||||||
|
|
||||||
function lang(){ try { return I18n.getLang ? I18n.getLang() : 'it'; } catch(e){ return 'it'; } }
|
|
||||||
function el(id){ return document.getElementById(id); }
|
|
||||||
function esc(s){ const d=document.createElement('div'); d.textContent=s==null?'':String(s); return d.innerHTML; }
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async function(){
|
|
||||||
if (typeof checkAuth==='function' && !checkAuth()) return;
|
|
||||||
if (window.I18n && I18n.init) I18n.init('it');
|
|
||||||
if (typeof loadSidebar==='function') loadSidebar();
|
|
||||||
if (window.HelpSystem && HelpSystem.init) HelpSystem.init();
|
|
||||||
await initStart();
|
|
||||||
});
|
|
||||||
|
|
||||||
async function initStart(){
|
|
||||||
try {
|
|
||||||
const cat = await api.acnCatalog();
|
|
||||||
ACN.level = cat.entity_level;
|
|
||||||
const lvlTxt = cat.entity_level === 'essential'
|
|
||||||
? '<span class="acn-level-badge acn-level-essential">'+(lang()==='en'?'ESSENTIAL entity':'Soggetto ESSENZIALE')+'</span>'
|
|
||||||
: '<span class="acn-level-badge acn-level-important">'+(lang()==='en'?'IMPORTANT entity':'Soggetto IMPORTANTE')+'</span>';
|
|
||||||
el('acn-level-info').innerHTML = lvlTxt +
|
|
||||||
'<p class="text-muted" style="margin-top:10px;">' +
|
|
||||||
(lang()==='en'?'Applicable scope: ':'Perimetro applicabile: ') +
|
|
||||||
'<strong>'+cat.totals.measures+'</strong> '+(lang()==='en'?'measures':'misure')+', <strong>'+
|
|
||||||
cat.totals.requirements+'</strong> '+(lang()==='en'?'requirements':'requisiti')+'.</p>';
|
|
||||||
window._acnCatalog = cat;
|
|
||||||
} catch(e){
|
|
||||||
if (e && e.error_code === 'ENTITY_LEVEL_REQUIRED') {
|
|
||||||
el('acn-level-info').innerHTML = '<p class="text-muted">'+(lang()==='en'
|
|
||||||
?'Classify the organization as important or essential first.'
|
|
||||||
:'Classifica prima il soggetto come importante o essenziale (Impostazioni › Classificazione NIS2).')+'</p>';
|
|
||||||
} else {
|
|
||||||
el('acn-level-info').innerHTML = '<p class="text-muted">'+(lang()==='en'?'Could not load.':'Caricamento non riuscito.')+'</p>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const list = await api.acnList();
|
|
||||||
if (list.assessments && list.assessments.length){
|
|
||||||
let html = '<h4 style="margin-bottom:8px;">'+(lang()==='en'?'Existing assessments':'Assessment esistenti')+'</h4>';
|
|
||||||
list.assessments.forEach(function(a){
|
|
||||||
const st = a.status==='completed'
|
|
||||||
? '<span class="badge badge-success">'+(lang()==='en'?'Completed':'Completato')+(a.overall_score!=null?(' · '+a.overall_score):'')+'</span>'
|
|
||||||
: '<span class="badge badge-warning">'+(lang()==='en'?'In progress':'In corso')+'</span>';
|
|
||||||
html += '<div style="display:flex; justify-content:space-between; align-items:center; gap:10px; padding:8px 0; border-bottom:1px solid var(--gray-100,#f3f4f6);">'+
|
|
||||||
'<div><strong>'+esc(a.title)+'</strong> '+st+'</div>'+
|
|
||||||
'<button class="btn btn-sm btn-outline" onclick="openAcn('+a.id+',\''+a.status+'\')">'+
|
|
||||||
(a.status==='completed'?(lang()==='en'?'View':'Vedi'):(lang()==='en'?'Resume':'Riprendi'))+'</button></div>';
|
|
||||||
});
|
|
||||||
el('acn-existing').innerHTML = html;
|
|
||||||
el('acn-existing').style.display='block';
|
|
||||||
}
|
|
||||||
} catch(e){}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function showCatalog(){
|
|
||||||
const cat = window._acnCatalog;
|
|
||||||
if (!cat){ return; }
|
|
||||||
let html = '';
|
|
||||||
(cat.functions||[]).forEach(function(f){
|
|
||||||
html += '<div class="acn-func open"><div class="acn-func-head" onclick="this.parentNode.classList.toggle(\'open\')">'+
|
|
||||||
'<strong>'+esc(FUNC_LABELS[f.function_code]?FUNC_LABELS[f.function_code][lang()]:f.function_name)+'</strong>'+
|
|
||||||
'<span class="acn-func-meta">'+(f.measures||[]).length+' '+(lang()==='en'?'measures':'misure')+'</span></div>'+
|
|
||||||
'<div class="acn-func-body">';
|
|
||||||
(f.measures||[]).forEach(function(m){
|
|
||||||
html += '<div class="acn-measure"><span class="acn-measure-code">'+esc(m.measure_code)+'</span>'+
|
|
||||||
'<span class="acn-measure-title">'+esc(m.measure_title)+'</span>';
|
|
||||||
(m.requirements||[]).forEach(function(r){
|
|
||||||
html += '<div class="acn-req"><span class="acn-req-idx">'+r.index+'.</span>'+esc(r.text)+'</div>';
|
|
||||||
});
|
|
||||||
html += '</div>';
|
|
||||||
});
|
|
||||||
html += '</div></div>';
|
|
||||||
});
|
|
||||||
el('acn-catalog-body').innerHTML = html;
|
|
||||||
el('acn-catalog').style.display='block';
|
|
||||||
el('acn-catalog').scrollIntoView({behavior:'smooth'});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createAcn(){
|
|
||||||
try {
|
|
||||||
const res = await api.acnCreate({});
|
|
||||||
await openAcn(res.id, 'draft');
|
|
||||||
} catch(e){
|
|
||||||
alert((e && e.message) || (lang()==='en'?'Creation failed':'Creazione non riuscita'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openAcn(id, status){
|
|
||||||
try {
|
|
||||||
ACN.id = id;
|
|
||||||
const res = await api.acnRequirements(id);
|
|
||||||
ACN.level = res.assessment.entity_level;
|
|
||||||
if (res.assessment.status === 'completed'){
|
|
||||||
await showResults(id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
renderWizard(res);
|
|
||||||
} catch(e){
|
|
||||||
alert((e && e.message) || (lang()==='en'?'Could not open the assessment.':'Impossibile aprire l\'assessment.'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderWizard(res){
|
|
||||||
el('acn-start').style.display='none';
|
|
||||||
el('acn-catalog').style.display='none';
|
|
||||||
el('acn-results').style.display='none';
|
|
||||||
el('acn-wizard').style.display='block';
|
|
||||||
el('btn-ai-analyze').style.display='none';
|
|
||||||
el('acn-wizard-level').innerHTML = ACN.level==='essential'
|
|
||||||
? '<span class="acn-level-badge acn-level-essential">'+(lang()==='en'?'ESSENTIAL':'ESSENZIALE')+'</span>'
|
|
||||||
: '<span class="acn-level-badge acn-level-important">'+(lang()==='en'?'IMPORTANT':'IMPORTANTE')+'</span>';
|
|
||||||
|
|
||||||
ACN.answers = {}; ACN.total = 0;
|
|
||||||
let html = '';
|
|
||||||
(res.functions||[]).forEach(function(f, fi){
|
|
||||||
let fcount = 0;
|
|
||||||
let inner = '';
|
|
||||||
(f.categories||[]).forEach(function(c){
|
|
||||||
(c.measures||[]).forEach(function(m){
|
|
||||||
inner += '<div class="acn-measure"><span class="acn-measure-code">'+esc(m.measure_code)+'</span>'+
|
|
||||||
'<span class="acn-measure-title">'+esc(m.measure_title)+'</span>';
|
|
||||||
(m.requirements||[]).forEach(function(r){
|
|
||||||
ACN.total++;
|
|
||||||
fcount++;
|
|
||||||
if (r.response) ACN.answers[r.key] = r.response;
|
|
||||||
inner += renderReq(r);
|
|
||||||
});
|
|
||||||
inner += '</div>';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
html += '<div class="acn-func'+(fi===0?' open':'')+'">'+
|
|
||||||
'<div class="acn-func-head" onclick="this.parentNode.classList.toggle(\'open\')">'+
|
|
||||||
'<strong>'+esc(FUNC_LABELS[f.function_code]?FUNC_LABELS[f.function_code][lang()]:f.function_name)+'</strong>'+
|
|
||||||
'<span class="acn-func-meta">'+fcount+' '+(lang()==='en'?'requirements':'requisiti')+'</span></div>'+
|
|
||||||
'<div class="acn-func-body">'+inner+'</div></div>';
|
|
||||||
});
|
|
||||||
el('acn-functions').innerHTML = html;
|
|
||||||
el('acn-total').textContent = ACN.total;
|
|
||||||
updateProgress();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderReq(r){
|
|
||||||
const opts = [
|
|
||||||
['implemented', lang()==='en'?'Implemented':'Attuato'],
|
|
||||||
['partial', lang()==='en'?'Partial':'Parziale'],
|
|
||||||
['not_implemented', lang()==='en'?'Not implemented':'Non attuato'],
|
|
||||||
['not_applicable', lang()==='en'?'N/A':'Non applic.']
|
|
||||||
];
|
|
||||||
let btns = '';
|
|
||||||
opts.forEach(function(o){
|
|
||||||
const sel = r.response===o[0] ? ('sel-'+o[0]) : '';
|
|
||||||
btns += '<button type="button" class="acn-opt '+sel+'" data-key="'+esc(r.key)+'" data-val="'+o[0]+'" onclick="pick(this)">'+o[1]+'</button>';
|
|
||||||
});
|
|
||||||
return '<div class="acn-req"><div class="acn-req-text"><span class="acn-req-idx">'+r.index+'.</span>'+esc(r.text)+'</div>'+
|
|
||||||
'<div class="acn-opts">'+btns+'</div></div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function pick(btn){
|
|
||||||
const key = btn.getAttribute('data-key');
|
|
||||||
const val = btn.getAttribute('data-val');
|
|
||||||
const wrap = btn.parentNode;
|
|
||||||
Array.prototype.forEach.call(wrap.querySelectorAll('.acn-opt'), function(b){
|
|
||||||
b.className = 'acn-opt';
|
|
||||||
});
|
|
||||||
btn.className = 'acn-opt sel-'+val;
|
|
||||||
ACN.answers[key] = val;
|
|
||||||
updateProgress();
|
|
||||||
scheduleSave(key, val);
|
|
||||||
}
|
|
||||||
|
|
||||||
let pendingSaves = {};
|
|
||||||
function scheduleSave(key, val){
|
|
||||||
pendingSaves[key] = val;
|
|
||||||
el('acn-savehint').textContent = lang()==='en'?'Saving…':'Salvataggio…';
|
|
||||||
if (saveTimer) clearTimeout(saveTimer);
|
|
||||||
saveTimer = setTimeout(flushSaves, 700);
|
|
||||||
}
|
|
||||||
async function flushSaves(){
|
|
||||||
const batch = Object.keys(pendingSaves).map(function(k){ return { requirement_key:k, response_value:pendingSaves[k] }; });
|
|
||||||
if (!batch.length) return;
|
|
||||||
pendingSaves = {};
|
|
||||||
try {
|
|
||||||
await api.acnRespond(ACN.id, { responses: batch });
|
|
||||||
el('acn-savehint').textContent = (lang()==='en'?'Saved ':'Salvato ')+new Date().toLocaleTimeString(lang()==='en'?'en-GB':'it-IT').slice(0,5);
|
|
||||||
} catch(e){
|
|
||||||
el('acn-savehint').textContent = lang()==='en'?'Save pending':'Salvataggio in sospeso';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateProgress(){
|
|
||||||
const answered = Object.keys(ACN.answers).length;
|
|
||||||
el('acn-answered').textContent = answered;
|
|
||||||
el('acn-bar-fill').style.width = (ACN.total? Math.round(answered/ACN.total*100):0)+'%';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function completeAcn(){
|
|
||||||
await flushSaves();
|
|
||||||
const answered = Object.keys(ACN.answers).length;
|
|
||||||
if (answered < ACN.total){
|
|
||||||
el('acn-wizard-msg').innerHTML = '<div class="alert alert-warning">'+
|
|
||||||
(lang()==='en'?('Missing '+(ACN.total-answered)+' answers.'):('Mancano '+(ACN.total-answered)+' risposte.'))+'</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await api.acnComplete(ACN.id);
|
|
||||||
await showResults(ACN.id);
|
|
||||||
} catch(e){
|
|
||||||
el('acn-wizard-msg').innerHTML = '<div class="alert alert-warning">'+esc((e&&e.message)||'Errore')+'</div>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function showResults(id){
|
|
||||||
const rep = await api.acnReport(id);
|
|
||||||
const a = rep.assessment;
|
|
||||||
el('acn-start').style.display='none';
|
|
||||||
el('acn-wizard').style.display='none';
|
|
||||||
el('acn-catalog').style.display='none';
|
|
||||||
el('acn-results').style.display='block';
|
|
||||||
el('btn-ai-analyze').style.display='inline-flex';
|
|
||||||
ACN.id = id;
|
|
||||||
el('acn-overall').textContent = a.overall_score!=null ? a.overall_score : '—';
|
|
||||||
el('acn-overall').style.color = scoreColor(a.overall_score);
|
|
||||||
|
|
||||||
let fs = '';
|
|
||||||
const fscores = a.function_scores || {};
|
|
||||||
Object.keys(FUNC_LABELS).forEach(function(fc){
|
|
||||||
if (fscores[fc]==null) return;
|
|
||||||
fs += '<div class="acn-func-score"><span>'+esc(FUNC_LABELS[fc][lang()])+'</span>'+
|
|
||||||
'<span class="acn-score-pill" style="background:'+scoreBg(fscores[fc])+'; color:'+scoreColor(fscores[fc])+';">'+fscores[fc]+'</span></div>';
|
|
||||||
});
|
|
||||||
el('acn-func-scores').innerHTML = fs;
|
|
||||||
|
|
||||||
let g = '';
|
|
||||||
(rep.gaps||[]).forEach(function(gap){
|
|
||||||
const tag = gap.response_value==='not_implemented'
|
|
||||||
? '<span class="badge badge-danger">'+(lang()==='en'?'Not implemented':'Non attuato')+'</span>'
|
|
||||||
: '<span class="badge badge-warning">'+(lang()==='en'?'Partial':'Parziale')+'</span>';
|
|
||||||
g += '<div style="padding:10px 0; border-bottom:1px solid var(--gray-100,#f3f4f6);">'+
|
|
||||||
'<span class="acn-measure-code">'+esc(gap.measure_code)+'</span> '+tag+
|
|
||||||
'<div style="font-size:.88rem; margin-top:4px;">'+esc(gap.requirement_text)+'</div></div>';
|
|
||||||
});
|
|
||||||
el('acn-gaps').innerHTML = g || '<p class="text-muted">'+(lang()==='en'?'No gaps. Fully compliant.':'Nessun gap: pienamente conforme.')+'</p>';
|
|
||||||
|
|
||||||
if (a.ai_summary){
|
|
||||||
el('acn-ai-body').innerHTML = '<div style="white-space:pre-wrap; font-size:.9rem;">'+esc(a.ai_summary)+'</div>';
|
|
||||||
el('acn-ai-card').style.display='block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function acnAiAnalyze(){
|
|
||||||
const btn = el('btn-ai-analyze');
|
|
||||||
btn.disabled = true;
|
|
||||||
const old = btn.textContent;
|
|
||||||
btn.textContent = lang()==='en'?'Analyzing…':'Analisi in corso…';
|
|
||||||
try {
|
|
||||||
const res = await api.acnAiAnalyze(ACN.id);
|
|
||||||
el('acn-ai-body').innerHTML = '<div style="white-space:pre-wrap; font-size:.9rem;">'+esc(res.ai_summary)+'</div>';
|
|
||||||
el('acn-ai-card').style.display='block';
|
|
||||||
el('acn-ai-card').scrollIntoView({behavior:'smooth'});
|
|
||||||
} catch(e){
|
|
||||||
alert((e&&e.message)||(lang()==='en'?'AI unavailable':'AI non disponibile'));
|
|
||||||
} finally {
|
|
||||||
btn.disabled=false; btn.textContent=old;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function scoreColor(s){ if(s==null)return '#6b7280'; if(s>=80)return '#16a34a'; if(s>=60)return '#65a30d'; if(s>=40)return '#f59e0b'; return '#dc2626'; }
|
|
||||||
function scoreBg(s){ if(s==null)return '#f3f4f6'; if(s>=80)return '#f0fdf4'; if(s>=60)return '#f7fee7'; if(s>=40)return '#fffbeb'; return '#fef2f2'; }
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1088,10 +1088,10 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/api.js?v=20260623"></script>
|
<script src="js/api.js?v=20260624"></script>
|
||||||
<script src="js/common.js?v=20260623"></script>
|
<script src="js/common.js?v=20260624"></script>
|
||||||
<script src="js/i18n.js?v=20260623"></script>
|
<script src="js/i18n.js?v=20260624"></script>
|
||||||
<script src="js/help.js?v=20260623"></script>
|
<script src="js/help.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
loadSidebar();
|
loadSidebar();
|
||||||
|
|||||||
@@ -154,8 +154,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260623"></script>
|
<script src="/js/api.js?v=20260624"></script>
|
||||||
<script src="/js/common.js?v=20260623"></script>
|
<script src="/js/common.js?v=20260624"></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). -->
|
||||||
@@ -166,8 +166,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260623"></script>
|
<script src="/js/i18n.js?v=20260624"></script>
|
||||||
<script src="/js/help.js?v=20260623"></script>
|
<script src="/js/help.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth check ───────────────────────────────────────────
|
// ── Auth check ───────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
+4
-4
@@ -366,8 +366,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260623"></script>
|
<script src="/js/api.js?v=20260624"></script>
|
||||||
<script src="/js/common.js?v=20260623"></script>
|
<script src="/js/common.js?v=20260624"></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). -->
|
||||||
@@ -378,8 +378,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260623"></script>
|
<script src="/js/i18n.js?v=20260624"></script>
|
||||||
<script src="/js/help.js?v=20260623"></script>
|
<script src="/js/help.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth & Init ─────────────────────────────────────────
|
// ── Auth & Init ─────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
@@ -349,14 +349,14 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/api.js?v=20260623"></script>
|
<script src="js/api.js?v=20260624"></script>
|
||||||
<script src="js/common.js?v=20260623"></script>
|
<script src="js/common.js?v=20260624"></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=20260618"></script>
|
||||||
<script src="js/i18n.js?v=20260623"></script>
|
<script src="js/i18n.js?v=20260624"></script>
|
||||||
<script src="js/help.js?v=20260623"></script>
|
<script src="js/help.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|
||||||
|
|||||||
@@ -183,12 +183,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260623"></script>
|
<script src="/js/api.js?v=20260624"></script>
|
||||||
<script src="/js/common.js?v=20260623"></script>
|
<script src="/js/common.js?v=20260624"></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=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260623"></script>
|
<script src="/js/i18n.js?v=20260624"></script>
|
||||||
<script src="/js/help.js?v=20260623"></script>
|
<script src="/js/help.js?v=20260624"></script>
|
||||||
<script src="/js/competenze.js?v=20260617"></script>
|
<script src="/js/competenze.js?v=20260617"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -382,8 +382,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260623"></script>
|
<script src="/js/api.js?v=20260624"></script>
|
||||||
<script src="/js/common.js?v=20260623"></script>
|
<script src="/js/common.js?v=20260624"></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). -->
|
||||||
@@ -394,7 +394,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260623"></script>
|
<script src="/js/i18n.js?v=20260624"></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>`; }
|
||||||
|
|||||||
@@ -143,8 +143,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260623"></script>
|
<script src="/js/api.js?v=20260624"></script>
|
||||||
<script src="/js/common.js?v=20260623"></script>
|
<script src="/js/common.js?v=20260624"></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). -->
|
||||||
@@ -155,8 +155,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260623"></script>
|
<script src="/js/i18n.js?v=20260624"></script>
|
||||||
<script src="/js/help.js?v=20260623"></script>
|
<script src="/js/help.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth check ───────────────────────────────────────────
|
// ── Auth check ───────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
@@ -32,14 +32,14 @@
|
|||||||
"on_enter": [{ "type": "navigate", "view": "assessment" }], "on_exit": []
|
"on_enter": [{ "type": "navigate", "view": "assessment" }], "on_exit": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3, "title": "Gap analysis ACN di secondo livello", "duration_ms": 48000,
|
"id": 3, "title": "Elenco Misure e Requisiti", "duration_ms": 48000,
|
||||||
"view_target": "acn-gap",
|
"view_target": "misure-requisiti",
|
||||||
"narration": { "text_it": "Qui approfondiamo i requisiti della Determina ACN del 2025, distinti per soggetti essenziali e importanti. La piattaforma traccia lo stato di ciascun requisito e ne calcola l'avanzamento.", "kb_article_slug": "demo:nis2-tour-2026:step3", "duration_ms_it": 48000 },
|
"narration": { "text_it": "Qui consultiamo l'elenco canonico delle misure di sicurezza di base e dei relativi requisiti previsti dalla Determinazione ACN del 2025, distinti per soggetti essenziali e importanti. Per ogni requisito sono indicati la procedura e il rischio di default, e l'organizzazione registra la propria valutazione di conformità.", "kb_article_slug": "demo:nis2-tour-2026:step3", "duration_ms_it": 48000 },
|
||||||
"highlights": [
|
"highlights": [
|
||||||
{ "at_ms": 7000, "selector": "#acn-bar-fill", "style": "pulse-blue", "duration_ms": 4000 },
|
{ "at_ms": 7000, "selector": "#mr-stats", "style": "pulse-blue", "duration_ms": 4000 },
|
||||||
{ "at_ms": 24000, "selector": "#acn-catalog", "style": "halo", "duration_ms": 5000 }
|
{ "at_ms": 24000, "selector": "#mr-list", "style": "halo", "duration_ms": 5000 }
|
||||||
],
|
],
|
||||||
"on_enter": [{ "type": "navigate", "view": "acn-gap" }], "on_exit": []
|
"on_enter": [{ "type": "navigate", "view": "misure-requisiti" }], "on_exit": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4, "title": "Gestione dei rischi", "duration_ms": 55000,
|
"id": 4, "title": "Gestione dei rischi", "duration_ms": 55000,
|
||||||
|
|||||||
@@ -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=20260623"></script>
|
<script src="js/api.js?v=20260624"></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');
|
||||||
|
|||||||
+4
-4
@@ -1142,8 +1142,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260623"></script>
|
<script src="/js/api.js?v=20260624"></script>
|
||||||
<script src="/js/common.js?v=20260623"></script>
|
<script src="/js/common.js?v=20260624"></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>
|
||||||
@@ -1153,8 +1153,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260623"></script>
|
<script src="/js/i18n.js?v=20260624"></script>
|
||||||
<script src="/js/help.js?v=20260623"></script>
|
<script src="/js/help.js?v=20260624"></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
|
||||||
|
|||||||
@@ -351,8 +351,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260623"></script>
|
<script src="/js/api.js?v=20260624"></script>
|
||||||
<script src="/js/common.js?v=20260623"></script>
|
<script src="/js/common.js?v=20260624"></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). -->
|
||||||
@@ -363,8 +363,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260623"></script>
|
<script src="/js/i18n.js?v=20260624"></script>
|
||||||
<script src="/js/help.js?v=20260623"></script>
|
<script src="/js/help.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth & Init ──────────────────────────────────────────────
|
// ── Auth & Init ──────────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ $controllerMap = [
|
|||||||
'review-schedule' => 'ReviewScheduleController', // A4 Fase 4.4 — Scadenziario centralizzato (revisioni periodiche GV.PO-02/GV.SC-07/PR.AT/DE.CM)
|
'review-schedule' => 'ReviewScheduleController', // A4 Fase 4.4 — Scadenziario centralizzato (revisioni periodiche GV.PO-02/GV.SC-07/PR.AT/DE.CM)
|
||||||
'framework' => 'FrameworkController', // Epic C / C1 — Elenco canonico Misure/Requisiti (cfg_nis2_*, read-only)
|
'framework' => 'FrameworkController', // Epic C / C1 — Elenco canonico Misure/Requisiti (cfg_nis2_*, read-only)
|
||||||
'assessments' => 'AssessmentController',
|
'assessments' => 'AssessmentController',
|
||||||
'acn-gap' => 'AcnAssessmentController', // Gap Analysis ACN (Det. 164179/2025, misure/requisiti)
|
|
||||||
'isms' => 'IsmsModelController', // Modello Organizzativo SGSI (ISO 27001/27017/27018 + SoA)
|
'isms' => 'IsmsModelController', // Modello Organizzativo SGSI (ISO 27001/27017/27018 + SoA)
|
||||||
'dashboard' => 'DashboardController',
|
'dashboard' => 'DashboardController',
|
||||||
'risks' => 'RiskController',
|
'risks' => 'RiskController',
|
||||||
@@ -271,19 +270,6 @@ $actionMap = [
|
|||||||
'POST:{id}/aiAnalyze' => 'aiAnalyze',
|
'POST:{id}/aiAnalyze' => 'aiAnalyze',
|
||||||
],
|
],
|
||||||
|
|
||||||
// ── AcnAssessmentController — Gap Analysis ACN (Determinazione 164179/2025) ──
|
|
||||||
'acn-gap' => [
|
|
||||||
'GET:catalog' => 'catalog',
|
|
||||||
'GET:list' => 'list',
|
|
||||||
'POST:create' => 'create',
|
|
||||||
'GET:{id}' => 'get',
|
|
||||||
'GET:{id}/requirements' => 'getRequirements',
|
|
||||||
'POST:{id}/respond' => 'saveResponse',
|
|
||||||
'POST:{id}/complete' => 'complete',
|
|
||||||
'GET:{id}/report' => 'getReport',
|
|
||||||
'POST:{id}/aiAnalyze' => 'aiAnalyze',
|
|
||||||
],
|
|
||||||
|
|
||||||
// ── IsmsModelController — Modello Organizzativo SGSI (ISO 27001/27017/27018) ──
|
// ── IsmsModelController — Modello Organizzativo SGSI (ISO 27001/27017/27018) ──
|
||||||
'isms' => [
|
'isms' => [
|
||||||
'GET:model' => 'getModel',
|
'GET:model' => 'getModel',
|
||||||
|
|||||||
@@ -910,8 +910,8 @@ curl -H <span class="str">"X-API-Key: nis2_TUA_CHIAVE"</span> \
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="js/common.js?v=20260623"></script>
|
<script src="js/common.js?v=20260624"></script>
|
||||||
<script src="js/i18n.js?v=20260623"></script>
|
<script src="js/i18n.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
function showTab(id) {
|
function showTab(id) {
|
||||||
document.querySelectorAll('.tab-btn').forEach((b, i) => {
|
document.querySelectorAll('.tab-btn').forEach((b, i) => {
|
||||||
|
|||||||
+4
-4
@@ -184,8 +184,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260623"></script>
|
<script src="/js/api.js?v=20260624"></script>
|
||||||
<script src="/js/common.js?v=20260623"></script>
|
<script src="/js/common.js?v=20260624"></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). -->
|
||||||
@@ -196,8 +196,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260623"></script>
|
<script src="/js/i18n.js?v=20260624"></script>
|
||||||
<script src="/js/help.js?v=20260623"></script>
|
<script src="/js/help.js?v=20260624"></script>
|
||||||
<script src="/js/isms.js"></script>
|
<script src="/js/isms.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -194,15 +194,6 @@ class NIS2API {
|
|||||||
}
|
}
|
||||||
return r.data;
|
return r.data;
|
||||||
}
|
}
|
||||||
acnCatalog() { return this._acn(this.get('/acn-gap/catalog')); }
|
|
||||||
acnList() { return this._acn(this.get('/acn-gap/list')); }
|
|
||||||
acnCreate(data) { return this._acn(this.post('/acn-gap/create', data || {})); }
|
|
||||||
acnGet(id) { return this._acn(this.get(`/acn-gap/${id}`)); }
|
|
||||||
acnRequirements(id) { return this._acn(this.get(`/acn-gap/${id}/requirements`)); }
|
|
||||||
acnRespond(id, data) { return this._acn(this.post(`/acn-gap/${id}/respond`, data)); }
|
|
||||||
acnComplete(id) { return this._acn(this.post(`/acn-gap/${id}/complete`, {})); }
|
|
||||||
acnReport(id) { return this._acn(this.get(`/acn-gap/${id}/report`)); }
|
|
||||||
acnAiAnalyze(id) { return this._acn(this.post(`/acn-gap/${id}/aiAnalyze`, {})); }
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
// Modello Organizzativo SGSI (ISO 27001/27017/27018) — vedi IsmsModelController
|
// Modello Organizzativo SGSI (ISO 27001/27017/27018) — vedi IsmsModelController
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
{ name: 'Dashboard', href: 'dashboard.html', icon: iconGrid(), i18nKey: 'nav.dashboard' },
|
{ name: 'Dashboard', href: 'dashboard.html', icon: iconGrid(), i18nKey: 'nav.dashboard' },
|
||||||
{ name: 'Compliance Journey', href: 'workflow.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"/></svg>' },
|
{ name: 'Compliance Journey', href: 'workflow.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"/></svg>' },
|
||||||
{ name: 'Gap Analysis', href: 'assessment.html', icon: iconClipboardCheck(), i18nKey: 'nav.gap_analysis' },
|
{ name: 'Gap Analysis', href: 'assessment.html', icon: iconClipboardCheck(), i18nKey: 'nav.gap_analysis' },
|
||||||
{ name: 'Gap Analysis ACN', href: 'acn-gap.html', icon: iconClipboardCheck(), i18nKey: 'nav.acn_gap' },
|
{ name: 'Misure e Requisiti', href: 'misure-requisiti.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h8a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"/></svg>', i18nKey: 'nav.framework' },
|
||||||
{ name: 'Modello SGSI (ISO 27001)', href: 'isms.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 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>', i18nKey: 'nav.isms' }
|
{ name: 'Modello SGSI (ISO 27001)', href: 'isms.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 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>', i18nKey: 'nav.isms' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -188,7 +188,6 @@ function loadSidebar() {
|
|||||||
{ name: 'Dashboard', href: 'dashboard.html', icon: iconGrid(), i18nKey: 'nav.dashboard' },
|
{ name: 'Dashboard', href: 'dashboard.html', icon: iconGrid(), i18nKey: 'nav.dashboard' },
|
||||||
{ name: 'Compliance Journey', href: 'workflow.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"/></svg>` },
|
{ name: 'Compliance Journey', href: 'workflow.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"/></svg>` },
|
||||||
{ name: 'Gap Analysis', href: 'assessment.html', icon: iconClipboardCheck(), i18nKey: 'nav.gap_analysis' },
|
{ name: 'Gap Analysis', href: 'assessment.html', icon: iconClipboardCheck(), i18nKey: 'nav.gap_analysis' },
|
||||||
{ name: 'Gap Analysis ACN', href: 'acn-gap.html', icon: iconClipboardCheck(), i18nKey: 'nav.acn_gap' },
|
|
||||||
{ name: 'Misure e Requisiti', href: 'misure-requisiti.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h8a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"/></svg>`, i18nKey: 'nav.framework' },
|
{ name: 'Misure e Requisiti', href: 'misure-requisiti.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h8a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"/></svg>`, i18nKey: 'nav.framework' },
|
||||||
{ name: 'Modello SGSI (ISO 27001)', href: 'isms.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 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>`, i18nKey: 'nav.isms' },
|
{ name: 'Modello SGSI (ISO 27001)', href: 'isms.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 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>`, i18nKey: 'nav.isms' },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
var VIEWS = {
|
var VIEWS = {
|
||||||
'dashboard': '/dashboard.html',
|
'dashboard': '/dashboard.html',
|
||||||
'assessment': '/assessment.html',
|
'assessment': '/assessment.html',
|
||||||
'acn-gap': '/acn-gap.html',
|
'misure-requisiti': '/misure-requisiti.html',
|
||||||
'risks': '/risks.html',
|
'risks': '/risks.html',
|
||||||
'incidents': '/incidents.html',
|
'incidents': '/incidents.html',
|
||||||
'policies': '/policies.html',
|
'policies': '/policies.html',
|
||||||
|
|||||||
@@ -114,51 +114,6 @@ const HelpSystem = (function () {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
'acn': {
|
|
||||||
title: 'Guida - Gap Analysis ACN (misure di base)',
|
|
||||||
intro: 'La Gap Analysis ACN valuta la conformita\' rispetto alle misure e ai requisiti PUNTUALI stabiliti dalla Determinazione ACN n. 164179 del 14/04/2025 (Allegati 1 e 2), non le 10 famiglie generiche dell\'art. 24 del D.Lgs. 138/2024 (art. 21 Direttiva). E\' l\'analisi di secondo livello, allineata all\'autovalutazione che i soggetti NIS compilano per l\'ACN.',
|
|
||||||
sections: [
|
|
||||||
{
|
|
||||||
heading: 'Importanti vs Essenziali',
|
|
||||||
items: [
|
|
||||||
'Il perimetro dipende dalla classificazione del soggetto: <strong>importante</strong> (Allegato 1) = <strong>37 misure / 92 requisiti</strong>; <strong>essenziale</strong> (Allegato 2) = <strong>43 misure / 116 requisiti</strong>.',
|
|
||||||
'La codifica dei requisiti e\' <strong>identica</strong> per i due livelli (stessi codici GV/ID/PR/DE/RS/RC): agli essenziali si aggiungono 6 misure e 24 requisiti, oltre a punti aggiuntivi su misure gia\' presenti.',
|
|
||||||
'L\'app filtra automaticamente i requisiti applicabili in base al livello del soggetto (Impostazioni › Classificazione NIS2).'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
heading: 'Struttura per funzione (Framework Nazionale)',
|
|
||||||
items: [
|
|
||||||
'I requisiti sono organizzati nelle 6 funzioni del Framework Nazionale: <strong>Governo (GV)</strong>, <strong>Identificazione (ID)</strong>, <strong>Protezione (PR)</strong>, <strong>Rilevamento (DE)</strong>, <strong>Risposta (RS)</strong>, <strong>Ripristino (RC)</strong>.',
|
|
||||||
'Per ogni requisito indichi lo stato: Attuato (100%), Parziale (50%), Non attuato (0%), Non applicabile (escluso dal calcolo).',
|
|
||||||
'Le risposte vengono salvate automaticamente. Puoi compilare in piu\' sessioni.'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
heading: 'Punteggio e piano d\'azione',
|
|
||||||
items: [
|
|
||||||
'Al completamento ottieni un punteggio complessivo 0-100 e un punteggio per ciascuna funzione FW.',
|
|
||||||
'Il <strong>piano d\'azione</strong> elenca i requisiti non conformi (non attuati prima, poi parziali), pronti per la remediation.',
|
|
||||||
'L\'<strong>analisi AI</strong> dei gap usa la knowledge base con i 203 requisiti ACN reali (grounding) e cita solo requisiti effettivamente esistenti.'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
heading: 'Rapporto con la Gap Analysis Art. 21',
|
|
||||||
items: [
|
|
||||||
'La Gap Analysis Art. 21 (le 10 lettere) e\' un\'analisi di <strong>primo livello</strong>, utile per orientarsi.',
|
|
||||||
'La Gap Analysis ACN e\' l\'analisi di <strong>conformita\' puntuale</strong> richiesta in concreto in Italia: usa questa per prepararti all\'autovalutazione ACN.'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
references: [
|
|
||||||
'Determinazione ACN n. 164179 del 14/04/2025 - Misure di sicurezza di base per i soggetti NIS',
|
|
||||||
'Allegato 1 - Misure di base per i soggetti IMPORTANTI (37 misure, 92 requisiti)',
|
|
||||||
'Allegato 2 - Misure di base per i soggetti ESSENZIALI (43 misure, 116 requisiti)',
|
|
||||||
'Framework Nazionale per la Cybersecurity e la Data Protection - funzioni GV/ID/PR/DE/RS/RC',
|
|
||||||
'Art. 24 D.Lgs. 138/2024 (recepisce l\'art. 21 della Direttiva (UE) 2022/2555) - base normativa primaria'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
'isms': {
|
'isms': {
|
||||||
title: 'Guida - Modello Organizzativo SGSI (ISO 27001)',
|
title: 'Guida - Modello Organizzativo SGSI (ISO 27001)',
|
||||||
intro: 'Il modulo Modello Organizzativo ti guida nella costruzione di un Sistema di Gestione della Sicurezza delle Informazioni (SGSI / ISMS) secondo ISO/IEC 27001:2022 (clausole 4-10) + lo Statement of Applicability (SoA) sui controlli Annex A. E\' uno strumento di SUPPORTO e PRE-AUDIT: non sostituisce l\'auditor ne costituisce una certificazione.',
|
intro: 'Il modulo Modello Organizzativo ti guida nella costruzione di un Sistema di Gestione della Sicurezza delle Informazioni (SGSI / ISMS) secondo ISO/IEC 27001:2022 (clausole 4-10) + lo Statement of Applicability (SoA) sui controlli Annex A. E\' uno strumento di SUPPORTO e PRE-AUDIT: non sostituisce l\'auditor ne costituisce una certificazione.',
|
||||||
@@ -1280,9 +1235,6 @@ const HelpSystem = (function () {
|
|||||||
'dashboard': 'dashboard',
|
'dashboard': 'dashboard',
|
||||||
'assessment.html': 'assessment',
|
'assessment.html': 'assessment',
|
||||||
'assessment': 'assessment',
|
'assessment': 'assessment',
|
||||||
'acn-gap.html': 'acn',
|
|
||||||
'acn-gap': 'acn',
|
|
||||||
'acn': 'acn',
|
|
||||||
'isms.html': 'isms',
|
'isms.html': 'isms',
|
||||||
'isms': 'isms',
|
'isms': 'isms',
|
||||||
'organigramma.html': 'org',
|
'organigramma.html': 'org',
|
||||||
@@ -1537,7 +1489,6 @@ const HelpSystem = (function () {
|
|||||||
var _guideAnchor = {
|
var _guideAnchor = {
|
||||||
'dashboard': 'cap-4',
|
'dashboard': 'cap-4',
|
||||||
'assessment': 'cap-5',
|
'assessment': 'cap-5',
|
||||||
'acn': 'cap-5b',
|
|
||||||
'risks': 'cap-6',
|
'risks': 'cap-6',
|
||||||
'incidents': 'cap-7',
|
'incidents': 'cap-7',
|
||||||
'policies': 'cap-8',
|
'policies': 'cap-8',
|
||||||
|
|||||||
@@ -19,24 +19,7 @@ const I18n = (function () {
|
|||||||
'nav.main': { it: 'Principale', en: 'Main' },
|
'nav.main': { it: 'Principale', en: 'Main' },
|
||||||
'nav.dashboard': { it: 'Dashboard', en: 'Dashboard' },
|
'nav.dashboard': { it: 'Dashboard', en: 'Dashboard' },
|
||||||
'nav.gap_analysis': { it: 'Gap Analysis', en: 'Gap Analysis' },
|
'nav.gap_analysis': { it: 'Gap Analysis', en: 'Gap Analysis' },
|
||||||
'nav.acn_gap': { it: 'Gap Analysis ACN', en: 'ACN Gap Analysis' },
|
|
||||||
'nav.framework': { it: 'Misure e Requisiti', en: 'Measures & Requirements' },
|
'nav.framework': { it: 'Misure e Requisiti', en: 'Measures & Requirements' },
|
||||||
// Gap Analysis ACN (Determinazione 164179/2025)
|
|
||||||
'acn.title': { it: 'Gap Analysis ACN (misure di base)', en: 'ACN Gap Analysis (baseline measures)' },
|
|
||||||
'acn.ai': { it: 'Analisi AI dei gap', en: 'AI gap analysis' },
|
|
||||||
'acn.intro.title': { it: 'Conformità puntuale alla Determinazione ACN 164179/2025.', en: 'Detailed compliance with ACN Determination 164179/2025.' },
|
|
||||||
'acn.intro.body': { it: "Questa analisi va oltre le 10 misure generiche dell'Art. 21: valuta le misure e i requisiti puntuali stabiliti dall'ACN per i soggetti NIS. Il perimetro dipende dalla classificazione del soggetto (importante o essenziale).", en: 'This analysis goes beyond the 10 generic Art. 21 measures: it assesses the specific measures and requirements set by ACN for NIS entities. The scope depends on the entity classification (important or essential).' },
|
|
||||||
'acn.new': { it: 'Nuova Gap Analysis ACN', en: 'New ACN Gap Analysis' },
|
|
||||||
'acn.catalog.btn': { it: 'Consulta il catalogo dei requisiti', en: 'Browse the requirements catalog' },
|
|
||||||
'acn.catalog.title': { it: 'Catalogo requisiti ACN applicabili', en: 'Applicable ACN requirements catalog' },
|
|
||||||
'acn.answered': { it: 'requisiti con risposta', en: 'requirements answered' },
|
|
||||||
'acn.complete': { it: 'Calcola punteggio e completa', en: 'Calculate score and complete' },
|
|
||||||
'acn.results.title': { it: 'Esito Gap Analysis ACN', en: 'ACN Gap Analysis result' },
|
|
||||||
'acn.results.overall': { it: 'Punteggio complessivo di conformità (0-100)', en: 'Overall compliance score (0-100)' },
|
|
||||||
'acn.results.byfunc': { it: 'Punteggio per funzione (Framework Nazionale)', en: 'Score by function (National Framework)' },
|
|
||||||
'acn.results.ai': { it: 'Analisi AI dei gap', en: 'AI gap analysis' },
|
|
||||||
'acn.results.gaps': { it: "Requisiti non conformi (piano d'azione)", en: 'Non-compliant requirements (action plan)' },
|
|
||||||
'acn.backstart': { it: "Torna all'inizio", en: 'Back to start' },
|
|
||||||
// ── Modello Organizzativo SGSI (ISO 27001) ──
|
// ── Modello Organizzativo SGSI (ISO 27001) ──
|
||||||
'nav.isms': { it: 'Modello SGSI (ISO 27001)', en: 'ISMS Model (ISO 27001)' },
|
'nav.isms': { it: 'Modello SGSI (ISO 27001)', en: 'ISMS Model (ISO 27001)' },
|
||||||
'isms.title': { it: 'Modello Organizzativo SGSI (ISO 27001)', en: 'ISMS Organizational Model (ISO 27001)' },
|
'isms.title': { it: 'Modello Organizzativo SGSI (ISO 27001)', en: 'ISMS Organizational Model (ISO 27001)' },
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* NIS2 Agile - Modello Organizzativo SGSI (ISO 27001/27017/27018)
|
* NIS2 Agile - Modello Organizzativo SGSI (ISO 27001/27017/27018)
|
||||||
* Wizard 6 step: Contesto → Leadership → Risk → SoA → Documenti → Monitoraggio.
|
* Wizard 6 step: Contesto → Leadership → Risk → SoA → Documenti → Monitoraggio.
|
||||||
* Pattern coerente con acn-gap.html: client api.* che ritorna `data` e lancia su errore.
|
* Pattern coerente con le altre pagine: client api.* che ritorna `data` e lancia su errore.
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -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=20260623"></script>
|
<script src="/js/common.js?v=20260624"></script>
|
||||||
<script src="/js/api.js?v=20260623"></script>
|
<script src="/js/api.js?v=20260624"></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>
|
||||||
@@ -162,8 +162,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260623"></script>
|
<script src="/js/i18n.js?v=20260624"></script>
|
||||||
<script src="/js/help.js?v=20260623"></script>
|
<script src="/js/help.js?v=20260624"></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.
|
||||||
|
|||||||
@@ -515,7 +515,7 @@ body { background: var(--bg-main); }
|
|||||||
|
|
||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
|
|
||||||
<script src="js/api.js?v=20260623"></script>
|
<script src="js/api.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
// ══════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════
|
||||||
// CONFIG
|
// CONFIG
|
||||||
|
|||||||
+2
-2
@@ -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=20260623"></script>
|
<script src="js/api.js?v=20260624"></script>
|
||||||
<script src="js/common.js?v=20260623"></script>
|
<script src="js/common.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
|
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
|
||||||
|
|
||||||
|
|||||||
@@ -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=20260623">
|
<link rel="stylesheet" href="/css/style.css?v=20260624">
|
||||||
<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=20260623"></script>
|
<script src="/js/api.js?v=20260624"></script>
|
||||||
<script src="/js/common.js?v=20260623"></script>
|
<script src="/js/common.js?v=20260624"></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=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260623"></script>
|
<script src="/js/i18n.js?v=20260624"></script>
|
||||||
<script src="/js/help.js?v=20260623"></script>
|
<script src="/js/help.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
'use strict';
|
'use strict';
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -424,6 +424,6 @@ curl https://nis2.agile.software/api/services/status</pre>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<script src="js/common.js?v=20260623"></script>
|
<script src="js/common.js?v=20260624"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -112,8 +112,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260623"></script>
|
<script src="/js/api.js?v=20260624"></script>
|
||||||
<script src="/js/common.js?v=20260623"></script>
|
<script src="/js/common.js?v=20260624"></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). -->
|
||||||
@@ -124,8 +124,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260623"></script>
|
<script src="/js/i18n.js?v=20260624"></script>
|
||||||
<script src="/js/help.js?v=20260623"></script>
|
<script src="/js/help.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
loadSidebar();
|
loadSidebar();
|
||||||
|
|||||||
@@ -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=20260623"></script>
|
<script src="js/api.js?v=20260624"></script>
|
||||||
<script src="js/common.js?v=20260623"></script>
|
<script src="js/common.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth check ──────────────────────────────────────────────────
|
// ── Auth check ──────────────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
@@ -138,12 +138,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260623"></script>
|
<script src="/js/api.js?v=20260624"></script>
|
||||||
<script src="/js/common.js?v=20260623"></script>
|
<script src="/js/common.js?v=20260624"></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=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260623"></script>
|
<script src="/js/i18n.js?v=20260624"></script>
|
||||||
<script src="/js/help.js?v=20260623"></script>
|
<script src="/js/help.js?v=20260624"></script>
|
||||||
<script src="/js/organigramma.js?v=20260617"></script>
|
<script src="/js/organigramma.js?v=20260617"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -333,8 +333,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260623"></script>
|
<script src="/js/api.js?v=20260624"></script>
|
||||||
<script src="/js/common.js?v=20260623"></script>
|
<script src="/js/common.js?v=20260624"></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). -->
|
||||||
@@ -345,8 +345,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260623"></script>
|
<script src="/js/i18n.js?v=20260624"></script>
|
||||||
<script src="/js/help.js?v=20260623"></script>
|
<script src="/js/help.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth & Init ─────────────────────────────────────────
|
// ── Auth & Init ─────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
+4
-4
@@ -131,12 +131,12 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260623"></script>
|
<script src="/js/api.js?v=20260624"></script>
|
||||||
<script src="/js/common.js?v=20260623"></script>
|
<script src="/js/common.js?v=20260624"></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=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260623"></script>
|
<script src="/js/i18n.js?v=20260624"></script>
|
||||||
<script src="/js/help.js?v=20260623"></script>
|
<script src="/js/help.js?v=20260624"></script>
|
||||||
<script src="/js/raci.js?v=20260619"></script>
|
<script src="/js/raci.js?v=20260619"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -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=20260623"></script>
|
<script src="js/api.js?v=20260624"></script>
|
||||||
<script src="js/common.js?v=20260623"></script>
|
<script src="js/common.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
|
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -443,8 +443,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260623"></script>
|
<script src="/js/api.js?v=20260624"></script>
|
||||||
<script src="/js/common.js?v=20260623"></script>
|
<script src="/js/common.js?v=20260624"></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). -->
|
||||||
@@ -455,8 +455,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260623"></script>
|
<script src="/js/i18n.js?v=20260624"></script>
|
||||||
<script src="/js/help.js?v=20260623"></script>
|
<script src="/js/help.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth & Init ─────────────────────────────────────────
|
// ── Auth & Init ─────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
@@ -141,12 +141,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260623"></script>
|
<script src="/js/api.js?v=20260624"></script>
|
||||||
<script src="/js/common.js?v=20260623"></script>
|
<script src="/js/common.js?v=20260624"></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=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260623"></script>
|
<script src="/js/i18n.js?v=20260624"></script>
|
||||||
<script src="/js/help.js?v=20260623"></script>
|
<script src="/js/help.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
'use strict';
|
'use strict';
|
||||||
/*
|
/*
|
||||||
|
|||||||
+4
-4
@@ -494,8 +494,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260623"></script>
|
<script src="/js/api.js?v=20260624"></script>
|
||||||
<script src="/js/common.js?v=20260623"></script>
|
<script src="/js/common.js?v=20260624"></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). -->
|
||||||
@@ -506,8 +506,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260623"></script>
|
<script src="/js/i18n.js?v=20260624"></script>
|
||||||
<script src="/js/help.js?v=20260623"></script>
|
<script src="/js/help.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth & Init ──────────────────────────────────────────────
|
// ── Auth & Init ──────────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
@@ -672,8 +672,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260623"></script>
|
<script src="/js/api.js?v=20260624"></script>
|
||||||
<script src="/js/common.js?v=20260623"></script>
|
<script src="/js/common.js?v=20260624"></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). -->
|
||||||
@@ -684,8 +684,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260623"></script>
|
<script src="/js/i18n.js?v=20260624"></script>
|
||||||
<script src="/js/help.js?v=20260623"></script>
|
<script src="/js/help.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth check ───────────────────────────────────────────
|
// ── Auth check ───────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
@@ -165,8 +165,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/api.js?v=20260623"></script>
|
<script src="js/api.js?v=20260624"></script>
|
||||||
<script src="js/common.js?v=20260623"></script>
|
<script src="js/common.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth check ───────────────────────────────────────────
|
// ── Auth check ───────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
@@ -124,12 +124,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260623"></script>
|
<script src="/js/api.js?v=20260624"></script>
|
||||||
<script src="/js/common.js?v=20260623"></script>
|
<script src="/js/common.js?v=20260624"></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=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260623"></script>
|
<script src="/js/i18n.js?v=20260624"></script>
|
||||||
<script src="/js/help.js?v=20260623"></script>
|
<script src="/js/help.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
'use strict';
|
'use strict';
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -477,8 +477,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260623"></script>
|
<script src="/js/api.js?v=20260624"></script>
|
||||||
<script src="/js/common.js?v=20260623"></script>
|
<script src="/js/common.js?v=20260624"></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). -->
|
||||||
@@ -489,8 +489,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260623"></script>
|
<script src="/js/i18n.js?v=20260624"></script>
|
||||||
<script src="/js/help.js?v=20260623"></script>
|
<script src="/js/help.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth & Init ─────────────────────────────────────────
|
// ── Auth & Init ─────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
+1
-1
@@ -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.2';
|
const CACHE = 'nis2-shell-v1.18.3';
|
||||||
|
|
||||||
const PRECACHE = [
|
const PRECACHE = [
|
||||||
'/offline.html',
|
'/offline.html',
|
||||||
|
|||||||
@@ -292,8 +292,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260623"></script>
|
<script src="/js/api.js?v=20260624"></script>
|
||||||
<script src="/js/common.js?v=20260623"></script>
|
<script src="/js/common.js?v=20260624"></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). -->
|
||||||
@@ -304,8 +304,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260623"></script>
|
<script src="/js/i18n.js?v=20260624"></script>
|
||||||
<script src="/js/help.js?v=20260623"></script>
|
<script src="/js/help.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth & Init ─────────────────────────────────────────
|
// ── Auth & Init ─────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"version": "1.18.2", "build": "2026-06-16-v1.18.2", "date": "2026-06-16", "changelog": "Epic C / C1 - fix post verifica flotta: copia procedure ora transazionale (SELECT FOR UPDATE, anti-duplicati su fallimento parziale/concorrenza); importer idempotente committato (application/cli/seed_framework.php); allineato conteggio requisiti importanti 92 (dato autoritativo Simon) in help/registry fonti; troncamento anteprima valutazione corretto (raw prima di esc); focus management modale (a11y). Additivo."}
|
{"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."}
|
||||||
|
|||||||
@@ -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=20260623"></script>
|
<script src="/js/api.js?v=20260624"></script>
|
||||||
<script src="/js/common.js?v=20260623"></script>
|
<script src="/js/common.js?v=20260624"></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). -->
|
||||||
@@ -219,8 +219,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260623"></script>
|
<script src="/js/i18n.js?v=20260624"></script>
|
||||||
<script src="/js/help.js?v=20260623"></script>
|
<script src="/js/help.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
loadSidebar();
|
loadSidebar();
|
||||||
|
|||||||
@@ -319,9 +319,9 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/api.js?v=20260623"></script>
|
<script src="js/api.js?v=20260624"></script>
|
||||||
<script src="js/common.js?v=20260623"></script>
|
<script src="js/common.js?v=20260624"></script>
|
||||||
<script src="js/i18n.js?v=20260623"></script>
|
<script src="js/i18n.js?v=20260624"></script>
|
||||||
<script>
|
<script>
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
loadSidebar();
|
loadSidebar();
|
||||||
|
|||||||
Reference in New Issue
Block a user