Files
nis2-agile/scripts/ingest-v3-ui-doc.php
DevEnv nis2-agileandClaude Opus 4.8 58b4bdbe5e [FIX] UI V3 — allineamento help/i18n/KB/ARIA al menu top-nav
Dopo il revamp V3, allineati i layer che descrivevano ancora la sidebar V2:
- topnav-v3.js: aggiunte le pagine orfane in nav (Matrice RACI → Struttura
  interna, Scadenziario → Monitoraggio, Compliance Journey → Guida) + cablato
  data-i18n su gruppi e voci del mega-menu (traduzione IT/EN come in V2).
- i18n.js: nuove chiavi nav V3 (gruppi mega-menu + connettori/whistleblowing/
  normative/cross-analysis/kb/architettura/simulazione/integrazioni/workflow/
  audit ACN/registra incidente).
- AIService.php navigationMapBlock: riscritta sul menu IN ALTO V3 (9 gruppi +
  pulsante "Registra incidente"), sync con topnav-v3.js NAV_GROUPS; ARIA non
  parla più di "barra laterale".
- help.js: corrette le 3 menzioni "sidebar" → barra/menu in alto.
- KB nis2_kb: nuovo docs/kb/V3_UI_NAVIGAZIONE.md + scripts/ingest-v3-ui-doc.php
  (ingerito host-side: 3 chunk SYSTEM, rimossi i chunk V2; ARIA RAG ora guida
  sul top-nav).
- Cache-buster ?v=20260627n sui 3 JS modificati su tutte le pagine.

Deploy: JS/HTML statici live via bind-mount; php-fpm reload (host + nis2-app)
per AIService.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 16:38:07 +02:00

153 lines
6.3 KiB
PHP

<?php
/**
* NIS2 Agile - Ingest documento "Guida UI V3 (top-nav)" nella KB (RAG)
* ----------------------------------------------------------------------------
* Indicizza docs/kb/V3_UI_NAVIGAZIONE.md nella collection Qdrant `nis2_kb` con
* scope SYSTEM, così che ARIA guidi l'utente nella nuova interfaccia V3 (menu in
* alto / mega-menu). SOSTITUISCE i chunk della guida UI V2 (Bootstrap Italia).
*
* ESEGUIRE dove Qdrant (172.21.0.5) + Voyage sono raggiungibili.
* Path proven (host Hetzner): il container nis2-app monta SOLO application/ e
* public/ (NON scripts/), quindi copiare temporaneamente script+doc sotto
* application/ ed eseguire via docker exec:
* cp scripts/ingest-v3-ui-doc.php application/_ingest-v3.php
* cp docs/kb/V3_UI_NAVIGAZIONE.md application/_v3_ui_doc.md
* docker exec -i nis2-app php /var/www/nis2-agile/application/_ingest-v3.php
* rm application/_ingest-v3.php application/_v3_ui_doc.md
* (lo script cerca il doc in più path: BASE_PATH/docs/kb, __DIR__, APP_PATH.)
*
* Idempotente: cancella i chunk SYSTEM della fonte V3 *e* della vecchia fonte V2
* prima del re-upsert. Opzioni: --dry-run (estrae e mostra le stat, niente upsert)
* ============================================================================
*/
if (PHP_SAPI !== 'cli') { fwrite(STDERR, "Solo CLI\n"); exit(1); }
if (!defined('BASE_PATH')) define('BASE_PATH', dirname(__DIR__));
if (!defined('APP_PATH')) define('APP_PATH', BASE_PATH . '/application');
require_once APP_PATH . '/config/env.php';
require_once APP_PATH . '/config/config.php';
require_once APP_PATH . '/config/database.php';
require_once APP_PATH . '/services/EmbedService.php';
require_once APP_PATH . '/services/VectorService.php';
$opts = getopt('', ['dry-run']);
$dryRun = isset($opts['dry-run']);
// Path doc: prova più candidati (root repo, accanto allo script copiato in application/)
$CANDIDATES = [
BASE_PATH . '/docs/kb/V3_UI_NAVIGAZIONE.md',
__DIR__ . '/V3_UI_NAVIGAZIONE.md',
__DIR__ . '/_v3_ui_doc.md',
APP_PATH . '/_v3_ui_doc.md',
];
$DOC_FILE = '';
foreach ($CANDIDATES as $c) { if (is_file($c)) { $DOC_FILE = $c; break; } }
$SOURCE = 'NIS2 Agile — Guida interfaccia V3 (top-nav)';
$TITLE = 'Interfaccia V3 NIS2 Agile — navigazione e moduli (menu in alto)';
$SOURCE_OLD = 'NIS2 Agile — Guida interfaccia V2 (Bootstrap Italia)'; // da rimpiazzare
function logln(string $m): void { echo '[' . date('Y-m-d H:i:s') . "] $m\n"; }
function chunkText(string $text, int $size = 2000, int $overlap = 200): array
{
$text = mb_convert_encoding($text, 'UTF-8', 'UTF-8');
$chunks = []; $len = mb_strlen($text, 'UTF-8'); $start = 0;
while ($start < $len) {
$take = min($size, $len - $start);
$piece = mb_substr($text, $start, $take, 'UTF-8');
if (trim($piece) !== '') $chunks[] = $piece;
if ($start + $take >= $len) break;
$start += ($size - $overlap);
}
return $chunks;
}
function uuid(): string
{
$b = random_bytes(16);
$b[6] = chr((ord($b[6]) & 0x0f) | 0x40);
$b[8] = chr((ord($b[8]) & 0x3f) | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($b), 4));
}
logln('=== Ingest Guida UI V3 nella KB (scope SYSTEM) ===');
if ($dryRun) logln('MODALITA DRY-RUN: nessun upsert.');
if ($DOC_FILE === '') { logln('ERRORE: doc V3 non trovato in nessun path candidato.'); exit(1); }
logln(' doc: ' . $DOC_FILE);
$text = (string) file_get_contents($DOC_FILE);
$text = preg_replace('/[ \t]+/', ' ', $text);
$text = preg_replace('/\n{3,}/', "\n\n", trim($text));
if (strlen($text) < 200) { logln('ERRORE: testo troppo breve.'); exit(1); }
$header = "DOCUMENTO: Guida all'interfaccia V3 di NIS2 Agile (menu in alto / mega-menu).\n"
. "USO: aiutare l'utente a navigare la UI, trovare moduli e funzioni.\n\n";
$chunks = chunkText($header . $text, 2000, 200);
logln(' testo: ' . strlen($text) . ' char -> ' . count($chunks) . ' chunk');
if ($dryRun) { logln('=== DRY-RUN completato ==='); exit(0); }
$embed = new EmbedService();
$vector = new VectorService();
$vector->ensureCollection($embed->dims);
// Idempotenza + supersede: rimuovi i chunk SYSTEM della fonte V3 e della vecchia V2
foreach ([$SOURCE, $SOURCE_OLD] as $src) {
try {
$vector->deleteByFilter(['must' => [
['key' => 'scope', 'match' => ['value' => 'SYSTEM']],
['key' => 'source', 'match' => ['value' => $src]],
]]);
logln(" rimossi chunk precedenti per source: {$src}");
} catch (Exception $e) { logln(' (warning) delete ' . $src . ': ' . $e->getMessage()); }
}
$docUuid = uuid();
$points = [];
foreach ($chunks as $i => $chunk) {
$vec = null;
for ($try = 1; $try <= 5; $try++) {
try { $vec = $embed->embed($chunk); break; }
catch (Throwable $e) {
if ($try === 5) { logln(" ERRORE embed chunk {$i} dopo 5 tentativi: " . $e->getMessage()); throw $e; }
logln(" retry embed chunk {$i} (tentativo {$try})");
sleep($try);
}
}
$points[] = [
'id' => uuid(),
'vector' => $vec,
'payload' => [
'doc_uuid' => $docUuid,
'title' => $TITLE . ($i > 0 ? ' (parte ' . ($i + 1) . ')' : ''),
'chunk' => $chunk,
'entity_type' => 'guida_prodotto',
'source' => $SOURCE,
'lang' => 'it',
'scope' => 'SYSTEM',
'consulting_firm_id' => null,
'organization_id' => null,
'shared_with_orgs' => [],
'uploaded_by' => 0,
],
];
}
foreach (array_chunk($points, 64) as $batch) {
$vector->upsertBatch($batch);
}
// Tracking MySQL (best-effort)
try {
$stmt = Database::getInstance()->prepare(
"INSERT INTO kb_uploaded_documents
(qdrant_doc_uuid, scope, consulting_firm_id, organization_id, uploaded_by, title, entity_type, source, lang, chunk_count, shared_with_orgs, status)
VALUES (?, 'SYSTEM', NULL, NULL, 0, ?, 'guida_prodotto', ?, 'it', ?, '[]', 'ready')"
);
$stmt->execute([$docUuid, $TITLE, $SOURCE, count($chunks)]);
} catch (Exception $e) { logln(' (warning) tracking insert: ' . $e->getMessage()); }
logln("=== OK indicizzato: doc_uuid={$docUuid}, " . count($chunks) . " chunk ===");