Files
nis2-agile/docs/CONTEXT_LAST_SESSION.md
T

108 KiB
Raw Blame History

Contesto Ultima Sessione

Il 2026-05-29 ci sono state DUE sessioni: pomeriggio e mattina (TRPG). Il 2026-05-30 sessione lunga: gap competitivi P1/P2/P3 + connettori + review multi-agente + fix.

2026-06-27 — 🟢 UI V3 (revamp top-nav) LIVE + allineamento help/i18n/KB/ARIA + mail tester + test completo + polish

Memorie: project-ui-v3-rollout, project_aria_chat_wiring, project-bootstrap-italia-rollout (superato).

A. Revamp UI V3 (top-nav navy/oro, Inter+Lucide self-host) — LIVE

Sostituita la sidebar Bootstrap Italia (V2) con una top-nav a mega-menu. Flusso: manutenzione flag-gated (.htaccess 503 + maintenance.html, bypass ?mnt=) → FASE 0 foundation (public/css/v3.css, public/js/topnav-v3.js, public/vendor/inter|lucide, dashboard pilota) commit b0fc190 → flotta di agenti su 32 pagine app (incl. admin/) commit a4150b5 → verifica (200/no-CDN/no-residui-V2) → rimossa manutenzione. v3.css ha un layer widget compat (modal/toast/form/btn-variant/spinner) + utility (.btn/.text-muted/.sr-only) + alias var V2→V3 così common.js funziona senza style.css. Reskin (wiring pagina preservato, id/data- intatti). Escluse standalone: supplier-assessment/simulate*/service-continuity. ⚠️ Manutenzione durata ~5h (vs ~1h stimata) per riavvii del processo che uccidevano gli agenti di sfondo (i file scritti però persistono).

B. Allineamento help / traduzioni / KB / ARIA al top-nav — commit 58b4bdb

Per evitare riferimenti alla vecchia sidebar: topnav-v3.js = aggiunte pagine orfane in nav (Matrice RACI→Struttura interna, Scadenziario→Monitoraggio, Compliance Journey→Guida) + cablato data-i18n; i18n.js = nuove chiavi nav V3 (gruppi + voci) IT/EN; AIService::navigationMapBlock riscritta sul menu in alto V3 (sync con topnav-v3.js NAV_GROUPS, non più common-bi.js) + reload php-fpm host+container; help.js = corrette 3 "sidebar"; KB nis2_kb = docs/kb/V3_UI_NAVIGAZIONE.md ingerito host-side (scripts/ingest-v3-ui-doc.php, 3 chunk SYSTEM, rimossi i chunk V2). Cache-buster ?v=20260627n sui 3 JS. ⚠️ aria-guide "Mostrami dov'è" → fallback pulito (naviga alla pagina, niente spotlight su mega-menu): rework opzionale.

C. Mail tester

Annuncio V3 "di nuovo online + cosa provare (menu in alto, Connettori Discovery, ARIA)" a m.tagliavini@/s.fattori@/presidenza@/cristiano.benassati@gmail.com (4×201 via send-raw). Finestra manutenzione protratta → scuse incluse.

D. Test completo di rilascio (puppeteer) + 2 bug trovati e fixati

T1–T6 tutti verdi: 36 pagine 200/no-CDN/no-residui-V2; tutto il JS (file + inline di 56 pagine) senza errori di sintassi; i18n completo (no chiavi mancanti app); 14 endpoint API con dati reali coerenti col DB e nessun leak multi-tenant; ARIA navigazione allineata al top-nav; pagine pubbliche 200 + manutenzione OFF. Render autenticato REALE con puppeteer-core (drive /usr/bin/google-chrome, pipe:true per aggirare il blocco porta-debug dell'env; token QA = mint jti+riga active_sessions; chiave org localStorage = nis2_org_id): top-nav si disegna (9 voci, 29 mega-link, FAB, utente/org), dati reali caricano, screenshot ok. 🐞 Bug 1 (commit a223923): ARIA data-aware MORTA via chat reale — AiController::ask chiamava requireAuth() ma non requireOrgAccess() → getCurrentOrgId() null → org_data_ok sempre falso. Fix: risolve l'org da X-Organization-Id→param→org primaria, gate membership anti-spoof. 🐞 Bug 2 (commit 95e77a6): titoli i18n V3 mr.title/normative.title mancanti → I18n.t() ritorna la chiave → l'<h1> mostrava "mr.title". Aggiunte le 2 chiavi; cache-buster i18n ?v=20260627p.

E. Polish 3 note minori — commit a71d1a0

  • aria-guide "Mostrami dov'è" ora funziona su V3: data-nav su voci+mega-link in topnav-v3.js; pointTo cerca in .bar-nav e apre il mega-menu (.aria-guide-open) puntando la voce; coach "menu in alto". Verificato a video (mega "Rischi" aperto, spotlight su "Inventario"). [supera la nota in B]
  • segnala-anonimo.html CSP font-src 'self' → 'self' data: (font data: sbloccato).
  • Cache-buster topnav-v3.js/common.js/aria-guide.js ?v=20260627t.
  • Ticket NIS2 (ticket-ms, tenant 7): coda VUOTA — 21 totali tutti RESOLVED/CLOSED, ultimo 25/6, nulla post-mail-V3. Gotcha: ?product=NIS2 ritorna 0 → listare senza filtro + header x-tenant-id:7, filtrare client-side su product.

F. Release v1.25.0 + 🐞 bug "sessione scaduta = pagina bloccata" (segnalato da Massimo)

  • v1.25.0 (commit 64e10b1): bump version.json (MINOR, UI V3) + cache SW nis2-shell-v1.25.0 con PRECACHE shell V3.
  • 🐞 Bug api.js (commit 41476c8) — Massimo "sono tornato su nis2 e non funziona": utente/aziende "--", inventario bloccato su Caricamento, niente FAB segnalazioni. NON è rottura V3: riprodotto con puppeteer che con token fresco tutto funziona (org-switcher con le sue 7 aziende, tabella inventario piena, 3 FAB). Causa = bug PRE-ESISTENTE in api.js: un 401 su /auth/refresh (refresh scaduto) rientrava nel handler di refresh (la condizione non escludeva l'endpoint auth) → ricorsione infinita, doRefreshToken() non ritorna mai → logout()/redirect a login non scatta → pagina resta "--". Scenario tipico del "torno dopo >2h" (access scaduto + refresh non valido). Fix: escludi /auth/refresh+/auth/login dall'auto-refresh + un 401 senza refresh disponibile fa logout(). Ora un utente che torna con sessione morta va a login (ri-accede). Cache-buster api.js ?v=20260627u su 41 pagine. Metodo diag: minti un token con exp nel passato (jti+active_sessions) e riproduci con puppeteer (pipe:true); chiave org localStorage = nis2_org_id. Mail a Massimo (+ copia a Cristiano, 2×201) in parole semplici: come rientrare (F5 → login → riaccedi), dove trova "Seleziona azienda"/inventario/FAB segnalazioni rosso, e consiglio di installare la PWA come app Windows ("Installa app"). Follow-up chiesto da Cristiano: rendere il rientro automatico (silent re-auth) così l'utente non veda nemmeno il login.

Commit di giornata (in ordine): b0fc190 (foundation+pilota), a4150b5 (flotta 32 pagine), 58b4bdb (allineamento), 9971fbc (context), a223923 (ARIA org-fix), 95e77a6 (i18n titoli), a71d1a0 (spotlight+CSP), e15e1b8 (context), 64e10b1 (v1.25.0+SW), 41476c8 (api.js sessione scaduta). Tutti pushati. Sessioni/token QA eliminati; working tree pulito.

2026-06-24 — 🟢 SESSIONE LUNGA: Vocea scaffold + segnalazioni Massimo + Agile=studio + UPGRADE ARIA (4 fasi) + avviso tester

Multi-tema. Memorie: project_aria_chat_wiring, project_ticketing_reporter, project_vocea_whistleblowing_integration, project_agile_tenant.

A. Vocea (whistleblowing zero-knowledge) — scaffold DORMIENTE

Vocea = white-label di nexus-whistleblowing-ms (AgileHub, NON voce/TTS). Decisione: sostituire il WB interno (plaintext) — Modalità C, no crypto re-impl. Scaffold committato (4e2e56e) + fix post-smoke (d45bd47): VoceaService.php (client X-API-Key api.vocea.cloud), mig 063 (NON applicata), config VOCEA_* (off di default), WhistleblowingController proxy/channelStatus. Smoke VIGILE/Vocea ha trovato 2 bug → corretti: tenantInfo rotta NON versionata /api/wb/tenants/{slug}/info; portalUrl=/{locale}/segnala/{slug}; vault namespace = tier1__nis2-app__vocea. BLOCCO a monte: deploy MS su api.vocea.cloud (route /integrations ancora 404 per VIGILE) + API key nel vault → poi attivare (mig 063 + VOCEA_ENABLED=true + slug org).

B. Segnalazioni Massimo Tagliavini — tutte RESOLVED

Le segnalazioni utente sono ticket AgileHub (ticket-ms 4213, tenant 7, product NIS2), non feedback_reports. Bug diagnosticato da VIGILE: JWT NIS2 senza email → caller_email vuoto → invisibili in «Le mie richieste» (fix common.js getMe + backfill VIGILE). GOTCHA query ticket-ms: serve header x-tenant-id: 7; GET /tickets/{id} include messages[]; usare GET /tickets/my?callerEmail=…. Io ho chiuso #424 (smoke ARIA OK) → i 10 ticket di Massimo ora tutti RESOLVED. Mail riepilogo inviata a m.tagliavini@ (201).

C. "Agile Technology" = studio di consulenza che gestisce sé stessa (commit 3b9f30a)

Consolidamento (no duplicati): consulting_firm 1 anagrafica REALE (P.IVA 07776161213, sede legale Roma Piazza di Campitelli 2, PEC agile.software@pec.it); org 996003 (Nuova Agile) → consulting_firm_id=1 + firm_org_assignments (prima azienda auto-gestita); 4 admin con consulting_firm_id=1 (Benassati 4 consulente, Silvia 103 presidenza@, Tagliavini 326, Fattori 330). Seeder application/cli/build_agile_consulente.php (idempotente, rollback nel file). Documenti SGSI già su 996003 (12 published).

D. UPGRADE ARIA — 4 fasi A→D (lamentela "AI non allineata con help + non vede i dati")

  • A (bafc305) Allineamento Help↔ARIA: help.js getContextText/detectPageId → common.js invia page_id+page_help → AIService::askWithRag inietta «GUIDA CONTESTUALE DELLA PAGINA» (fonte unica help.js).
  • D (4252904) Puntatore guida: public/js/aria-guide.js (NUOVO) + data-nav in common-bi.js → bottone «📍 Mostrami dov'è» con spotlight sulla voce di menu.
  • B (fe7d055) ARIA data-aware: AIService::orgDataSnapshotBlock (score/rischi/incidenti/asset/fornitori/NC/formazione/policy) iniettato SOLO se org_data_ok (AiController verifica membership user_organizations, super_admin bypassa; scope org → no leak cross-org).
  • C (a982797) Voce naturale: AiController::tts() + route POST /api/ai/tts proxy a nexus-voice-ms (172.21.0.1:4215, voce "Sarah") + speak()/toggle 🔊 in common.js. Smoke: voce-ms da nis2-app 200 audio/mpeg. Riferimento: trpg docs/changes/CHANGE_aria_voice_pointer.md (ALLTAX non leggibile dal devenv). Cache-buster common.js→20260624g/help/common-bi. php-fpm reloadato.

E. Altro

  • Cron supervisore NIS2 → ogni 90 min (5 0-23/3 + 35 1-23/3, TEMPORANEO, backup .nis2-supervisor.bak-17h4-20260623).
  • Avviso tester inviato (4 destinatari, 201) — voce/guida/dati ARIA + segnalazioni risolte + Ctrl+F5.
  • Memoria: nuovi project_ticketing_reporter, project_vocea_whistleblowing_integration, reference_db_gotchas; aggiornati project_agile_tenant, project_aria_chat_wiring; MEMORY.md compattato (251→37 righe).

Problemi aperti / prossimi passi

  1. Vocea: attendere deploy MS + API key vault → attivare (mig 063 + flag + slug org pilota da presidenza).
  2. ARIA polish: regola "non citare nomi file .html"; smoke in-app voce+puntatore (browser, Ctrl+F5).
  3. Cron 90 min temporaneo → riportare a 4h/oraria quando cala l'ondata.
  4. VIGILE: implementare sentinella OPEN-fermi (ok dato); #384 orfano lasciato (no attribuzione affidabile).
  5. mig 063 da applicare all'attivazione Vocea.

2026-06-20 — 🟢 SESSIONE LUNGA: completamento UI V2 + cliente "Nuova Agile" end-to-end + feature gestione documentale ISMS

Sessione molto lunga e multi-tema. Riepilogo per blocchi. Memorie: project_client_nuova_agile, project_isms_module, project_bootstrap_italia_rollout.

A. Completamento UI V2 (Bootstrap Italia/AGID) — v1.23.10 (commit 7ad5d68, 72d82d9)

Migrate a BI le ultime pagine: architecture.html + workflow.html (app-page: CSS BI + bundle JS + common-bi.js per sidebar V2; workflow ha cablato anche l'help "?"); integrazioniext.html + mktg-api-doc.html (doc pubblici: solo CSS BI). Rimossa setup-org.html (morta). Scoperto: il funnel login/register/onboarding era GIÀ su BI (le pagine pubbliche caricano la CSS BI ma NON common-bi.js perché senza sidebar → NON classificarle con common.js-vs-common-bi.js). Corretto docs/kb/V2_UI_NAVIGAZIONE.md (era stale: descriveva il vecchio approccio -bi.html ABBANDONATO; BI_PAGES ora mappa ogni pagina a sé) — commit f9b2c9d. Corrette 3 memorie stale: supplier portal (è COMPLETO/LIVE), allineamento TRPG (5 fasi COMPLETATE), ui_v2 dark/Inter (SUPERATA: il V2 è BI).

B. Cliente "Nuova Agile Technology srl" (org 996003) — creato live end-to-end

Software house cloud-only, modello work-from-anywhere. Persone = account ESISTENTI (Garretto 103/presidenza@, Tagliavini 326, Fattori 330/s.fattori@) collegati via user_organizations (NON duplicati). Seed idempotenti application/cli/build_nuova_agile_p1..p9.php (eseguiti via SSH host→docker exec nis2-app php):

  • p1: org + classif. NIS2 (important, sub_threshold_candidate/preliminary) + organigramma 8 ruoli.
  • p2: 12 asset (cloud, NIS2-rilevanti) + 4 fornitori + 10 rischi + gap analysis (45%).
  • p3a: ISMS model #3 (flag cloud true) + SoA 111 (93/7/11) + isms_roles · p3b: 31 documenti SGSI (Manuale/politiche/procedure/istruzioni, AI-redatti fonti-certe).
  • p4: 15 piani di controllo + 5 corsi. ⚠️ Classif. "importante" = autovalutazione preliminare (sotto soglie; caveat nel Manuale).

C. Simulazione audit + bonifica (combinato ISO 27001 + NIS2)

AUD-001 (internal_audits): 21 voci checklist + 5 NC (1 maggiore: determinazione applicabilità NIS2 non formalizzata; 4 minori). Bonifica (p5): 10 trattamenti rischio + 11 formazioni completate + 15 esecuzioni controlli + SoA avanzata + DPO interim + 5 CAPA → chiuse 3 NC operative; restano aperte 2 di governance (decisioni umane). Gap analysis → 80%.

D. Login Silvia (presidenza@)

SSO_MODE=local → login usa password LOCALE. Impostata password locale user 103 ("Silvia1978!"); regge perché la sua identità SSO centrale è password_version=1 (il cron sync agisce solo su password_version>1 → la salta). Test login API: 200 OK.

E. FEATURE PRODOTTO: Gestione documentale ISMS (ISO 27001 cl.7.5)

  • v1.24.0 (mig.060, commit 68694a0): ciclo di vita Bozza→Revisione→Approvato→Pubblicato→Archiviato + isms_document_versions (snapshot); 8 endpoint /api/isms/documents/{id}/(submit|approve|publish|reject|archive|newVersion|versions|word); export Word .doc (blob-fetch); UI in isms.js (badge/pulsanti/storico, IT/EN inline).
  • v1.24.1 (d29195b): download UNICO set (/api/isms/documentsWordAll, copertina+indice+page-break) + editor in-app (contenteditable+toolbar+toggle HTML; getDocument; guard NOT_EDITABLE sui pubblicati).
  • v1.24.2 (mig.061, e245e76): PERMESSI DOCUMENTALI profilabili per ruolo (isms_doc_permissions, requireDocCapability(), GET/PUT /api/isms/docPermissions, UI matrice in settings.html tab "Permessi documentali"). ⚠️ switchTab usa INDICI in tabMap.
  • v1.24.3 (7b8a604): help + i18n matrice IT/EN + ingest KB AI docs/kb/GESTIONE_DOCUMENTALE_SGSI.md (Qdrant nis2_kb SYSTEM, ricercabilità verificata).

F. Modello agile/work-from-anywhere + consolidamento documentale (Nuova Agile)

  • p7 (6ca78f9): recepito work-from-anywhere (no sede, dispositivi dedicati, separazione privato/lavoro, posta aziendale, key escrow chiavi cloud). 5 doc agili PUBBLICATI + scope SGSI riscritto + 4 rischi R-011..014. (Ricerca web: confermato ISO 27001 lean/agile per startup cloud.)
  • p8 (c05520c): CONSOLIDAMENTO 36→12. I 36 v1.0 fusi per tema (subagent dai contenuti reali) in 12 doc v2.0 PUBBLICATI (Manuale + 6 politiche + 5 procedure); 36 originali ARCHIVIATI. Gotcha: idempotenza deve ignorare gli archiviati (collisione titolo procedura incidenti).
  • p9 (27bbed3): allineati i riferimenti incrociati nei 12 (rename "Politica Generale", rinvii→"Governance del SGSI", istruzione hardening assorbita, sez.8 Manuale sui 12 doc) + snapshot v2.0. Auto-referenze interne minuscole lasciate.

Stato finale doc Nuova Agile: 12 PUBBLICATI v2.0 + 36 ARCHIVIATI v1.0; 14 rischi; SoA 111.

G. Coda sessione: task #8/#9/#10 + determinazione NIS2 + chiusura NC Nuova Agile

  • #9 (commit 3b1d909): re-ingest in nis2_kb della guida UI V2 CORRETTA (7 chunk, rimossi i chunk stale -bi.html del 12/6). Script application/cli/ingest_v2nav_kb.php.
  • #10 (mig.062, v1.24.4, commit 14c4c2a): version-provenance segnalazioni — feedback_reports += client_surface/client_version/client_build; FeedbackController+Service li salvano; feedback.js li invia (surface web-v2 + version/build da version.json). NB: feedback.js è DORMIENTE (rimosso dall'iniezione 14/6) → plumbing pronto.
  • #8a AI consulente normativo: ✅ verificato (/api/ai/ask→askWithRag cita art.25 D.Lgs.138/2024, 24h/72h/1mese, 5 fonti). #8c chiave mktg: ✅ moot (api_keys VUOTA). #8b (commit 05a1436, v1.24.5): i18n EN modulo Fornitori — supplier-portal.html (esterno OTP) bilingue self-contained (dict inline SPI18N+toggle sp_lang); supply-chain.html esteso (5→25 data-i18n+97 I18n.t); i18n.js +82 chiavi sp.* (→120).
  • Determinazione NIS2 Nuova Agile (p10, commit 51dc757): dati Direzione fatturato 0,9M€(2026)/1,4M€(2027) → SOTTO-SOGLIA; cloud provider (Allegato I) ma cap dimensionale → NON obbligata per legge; la Direzione DELIBERA l'adozione VOLONTARIA integrale come "importante" (org voluntary_compliance=1, designation_basis=voluntary, turnover=900k; Manuale sez.11 'Caveat'→'Determinazione'). NCR-9001 maggiore CHIUSA.
  • P.IVA + DPO (p11): P.IVA 07776161213 (vat+fiscal); DPO esterno Cristiano Benassati (user 4) nominato (org_role+isms_role+Manuale+membership auditor). NCR-9005 CHIUSA → TUTTE LE 5 NC CHIUSE (0 aperte).
  • Sedi + anagrafica completa (p12): sede legale Roma (Piazza di Campitelli 2, 00186) in address/city; sede operativa Modena (Strada Scaglia Est 15, 41121) in mission; scope SGSI reso preciso (sedi registrate + modello work-from-anywhere). Anagrafica Nuova Agile COMPLETA.
  • Escalation Simon (unico residuo, in carico a Simon): mail inviata a s.fattori@agile.software con le richieste ACN aperte → (1) requisiti soggetti "importanti" 92 vs 87 (prodotto usa 37 misure/92 req. da nis2_sources.php per Det.164179/2025 All.1; ACN ufficiale 87 — divergenza già annotata nel codice come "da riconciliare"); (2) conferma date Determinazioni ACN 164179 (14/4/25), 333017 (22/9/25, superata), 379887 (vigente, eff. 31/12/25). Alla risposta → aggiornare application/config/nis2_sources.php.

Note accesso/deploy

Host: chiave SSH effimera .ssh-temp/agilehub-host-temp (host dfm-dev/172.18.0.1). PROD = php-fpm HOST → reload .php = systemctl reload php8.4-fpm + USR2 nis2-app. Git push diretto dal devenv (helper vault). Prossima mig = 063.


2026-06-18 — 🟢 Hardening integrità DB (mig.058) — v1.23.1

Richiesta utente: "analizzare DB e chiavi per garantire integrità db". Esteso l'audit chiavi della mig.039 (12/6, fermo a ~tab.039) ai moduli 040-057 non ancora coperti. Memoria: project_db_integrity.

Stato base sano: 103 tabelle, tutte con PK, tutte InnoDB. Sonda live read-only nuova: application/cli/db_integrity_probe.php (PK/engine/UNIQUE/FK/orfani + pre-check duplicati (org,code); etichetta audit_logs come by-design, non bloccante).

mig.058 applicata (runner guardato application/cli/migrate_058_integrity_keys.php, idempotente, 15/15 OK, re-run = 12 SKIP):

  • TIER 1 additivo (0 dup/0 orfani verificati): +4 UNIQUE [internal_audits/management_reviews (org,code), kb_uploaded_documents.qdrant_doc_uuid, whistleblowing_reports.anonymous_token (drop idx_token ridondante)] + retry-on-1062 nei controller InternalAudit/ManagementReview (allineati a periodic_controls); +6 FK [consulting_firm_id su organizations/users/kb_uploaded_documents→consulting_firms; isms_soa.linked_control_id→compliance_controls; isms_documents.linked_policy_id→policies; management_review_decisions.capa_id→capa_actions, tutte SET NULL].
  • TIER 2 bonifica (confermata dall'utente, backup pre-DELETE in .backups/mig058_pre_cleanup_20260618-075418.sql): cancellate 9 righe stale firm_org_assignments→org 126-129 (eliminate nella pulizia demo 12/6; chiude il deferred-b mig.039) + 7 sessioni scadute active_sessions→org inesistenti, poi 2 FK CASCADE.
  • FK totali 196 → 204. Verdetto sonda post-fix: 0 problemi bloccanti (resta solo audit_logs by-design: trail immutabile mig.006). Smoke prod: api-status 200, controller list 401 (caricano OK post-reload).

By-design lasciato: audit_logs (no FK, immutabile), sentinel supplier_categories.org=0 (deferred-a mig.039), colonne polimorfiche (object_id/entity_id/...) e cross-DB (sso_identity_id/lg231_*).

Deploy: reload opcache systemctl reload php8.4-fpm (PROD=host) + USR2 nis2-app. version.json → 1.23.1. Prossima mig = 059. Pulizia: rimossi 4 file * copy.html residui in docs/nis2/.


2026-06-17 (seguito) — 🟢 Modulo CONTROLLI PERIODICI (4° modulo ISO-readiness, §9.1/A.8.16) — v1.23.0, mig.057, commit 4fdeae7

Richiesta utente: "oltre al modulo audit un modulo controlli con frequenza e generazione nc o ac" + "fai analisi anche di questo modulo / documentati con altri esempi" → analisi CCM (Continuous Control Monitoring, pattern Eramba/Drata/Vanta) in docs/DESIGN_CONTROLLI_PERIODICI.md. Memoria: project-iso-readiness-modules (punto D).

Cosa fa: registro dei controlli RICORRENTI (owner=ruolo organigramma, frequenza giornaliero..annuale o personalizzata in giorni, metodo, controllo SoA/NIS2 collegato) + registro esecuzioni = EVIDENZA (data/esito/note). La scadenza avanza ad ogni esecuzione; da esito non conforme/parziale → genera NC o NC+azione correttiva (riuso NCR/CAPA, nuova source='monitoring'). Scheda+storico stampabile; scadenze nel Calendario unico.

  • mig.057: periodic_controls + periodic_control_executions (FK CASCADE, UNIQUE (organization_id, code)); ALTER non_conformities.source += 'monitoring'. DDL autoritativa = seeder application/cli/seed_periodic_controls.php (idempotente; il .sql è solo doc-record e NON ha lo UNIQUE).
  • PeriodicControlController (slug periodic-controls) + controlli-periodici.html + CalendarController::srcPeriodicControls (type periodic_control).

Costruito + verificato con FLOTTE (richiesta "agenti al massimo"): build + flotta di verifica avversariale (18 agenti, 5 dimensioni → skeptics per finding) → 9 finding confermati, TUTTI corretti:

  • HIGH advance() overflow: strtotime('+1 month') saltava febbraio (31/01→03/03) → riscritto con DateTimeImmutable + clamp ultimo giorno valido (mensile/trim/sem/annuale; gg/sett/custom invariati). Verificato in prod: 31/01→28/02, 30/11 trim→28/02, 29/02 bisestile annuale→28/02.
  • HIGH review_schedule sync mai implementato → rimossa la cleanup morta in delete() (il calendario legge periodic_controls diretto, non usa review_schedule).
  • Pre-esistente sanato: CalendarController::srcReviewSchedule faceva doppia comparsa di internal_audit/stakeholder_activity (già con sorgente diretta) → entity_type NOT IN ('internal_audit','stakeholder_activity').
  • Minori: retry-on-1062 sul codice CTL-NNN; array_key_exists per azzerare owner_role_id/next_due_date; TYPE_META periodic_control nel calendario (chip/label/colore).
  • ⚠️ Residuo NON corretto (follow-up): ManagementReviewController::nextCode() stesso pattern MAX+1 senza UNIQUE (race low-sev).

Smoke prod OK (org 151, poi ripulita 0 residui): CRUD, esecuzione+avanzamento, NC/NC+AC, report 200, calendario; overflow date verificato; UNIQUE uk_pctl_code presente; azzeramento next_due via update.

Routine di chiusura completata: help.js + i18n IT/EN (già in build) + KB SYSTEM uuid 70e850da + email tester (stile prodotto). Cache-buster ?v=20260631, SW nis2-shell-v1.23.0. Deploy: seeder via SSH→docker exec nis2-app php, reload systemctl reload php8.4-fpm host (PROD = php-fpm host) + USR2 container. Commit+push diretto dal devenv. Prossima mig = 058.

Inoltre 17/6: inviata a Simon mail di riepilogo chiusura Epic C (C1–C5 tutti LIVE; chiede conferma 92↔87 requisiti importanti + data esatta Det.ACN 333017/2025).


2026-06-17 — 🟢 ISO-readiness: 3 moduli LIVE (Audit interni §9.2 + Riesame Direzione §9.3 + Calendario scadenze) — v1.22.0, mig.055-056, commit 79ca72f

Nati dalla valutazione ISO 27001 per l'audit di Agile (checklist in docs/Agile 27000/, valutazione docs/Agile 27000/VALUTAZIONE_COPERTURA_NIS2AGILE_vs_ISO27001.md): mancavano gli output gestionali 9.2 e 9.3. Design: docs/DESIGN_AUDIT_INTERNI_RIESAME_DIREZIONE.md. Memoria: project-iso-readiness-modules.

Costruiti con FLOTTE PARALLELE (richiesta utente "agenti al massimo"): workflow build (3 agenti, file non sovrapposti, ritorno patch per i file condivisi che ho integrato io) + workflow di verifica avversariale (28 agenti, 4 dimensioni × 2 lenti).

  • A — Audit interni (§9.2, mig.055): internal_audits+internal_audit_items; InternalAuditController slug internal-audits; internal-audits.html. AUD-NNN, checklist pre-popolata (clausole 4-10 + Annex A dal SoA), esiti per riga, apertura NC→NCR/CAPA, report stampabile, data→calendario (ENUM review_schedule += internal_audit).
  • B — Riesame Direzione (§9.3, mig.056): management_reviews+management_review_decisions; ManagementReviewController slug management-reviews; management-review.html. RD-AAAA-NN, gather aggrega gli input dai moduli, decisioni, approve→snapshot congelato, verbale stampabile.
  • C — Calendario scadenze (NESSUNA mig): CalendarController slug calendar (events/summary), aggregatore sola-lettura di tutte le scadenze; calendario.html (griglia+lista+filtri+deep-link).

Verifica flotta → 9 finding, TUTTI corretti: MAJOR gatherRisks (usava risk_treatments.organization_id inesistente → JOIN su risks); nextCode numerico; footer report audit "ISO buona prassi non obbligo"; help 24→25 clausole; ARIA tab/calendario + focus modali + tasti celle; rimossi 2 helper api morti.

Smoke prod OK: calendario 18 eventi; audit AUD-001 25 item + report + audit→NCR + audit→calendario; riesame gather/decisione/approve/report; gatherRisks ora available. Org 151 ripulita.

Routine di chiusura completata (help.js 3 sezioni + i18n IT/EN + KB SYSTEM uuid a04c8564 + email tester Fattori/Tagliavini/Mascagni, stile prodotto via relay). Cache-buster ?v=20260630, SW nis2-shell-v1.22.0. Prossima mig = 057.

Gap ISO residui (NON software, per la direzione): controlli tecnici A.7/A.8 sull'infrastruttura reale + alcuni atti formali; ISO resta volontaria (gli obblighi sono NIS2/D.Lgs.138/2024).


2026-06-16 (sera) — 🟢 EPIC C COMPLETO: C5 Stakeholder LIVE (C5.1 + C5.2 + hardening) — commit c782aa5→b917d2d, v1.19.0→1.21.1

Chiuso l'ultimo punto di Simon. Memoria: project-epic-c-simon. Tutto deployato in prod, smoke verde, committato+pushato (push diretto dal devenv).

C5.1 — Registro Stakeholder + matrice di Mendelow ✅ (commit c782aa5, mig.051, v1.19.0)

  • Decisione di design (si discosta dall'handoff): tabella dedicata stakeholders (NON colonna su suppliers): gli stakeholder interni non sono fornitori. Interni→org_roles; esterni→link opzionale a suppliers (no duplicazione).
  • Config: cfg_stakeholder_types (30 Stak.01-30 + org-added da Stak.31, code=PK) + cfg_stakeholder_quadrants (4, Q4 potere 0-2 = refuso corretto) + m2m stakeholder_procedures. StakeholderController (slug stakeholders); quadrante calcolato a read-time; anti-IDOR completo. stakeholders.html = registro + matrice Mendelow SVG dependency-free. 🐛 voce "Stakeholder" mancava in common-bi.js (sidebar V2) → aggiunta.

C5.2 — Attività, questionari, calendario, portale esterno ✅

  • Decisioni utente: portale esterno self-service ORA (magic-link, email off) + modello template dedicato leggero.
  • C5.2a (a5ff29e, mig.052, v1.20.0): questionari tipo (stk_questionnaire_templates, kind questionnaire/read_ack) + m2m a procedure/misure/requisiti; stk_activities+stk_activity_targets+stk_activity_procedures; assegnazione by_code/individuale; calendario via review_schedule esteso (ENUM += 'stakeholder_activity'). StakeholderActivityController (slug stakeholder-activities); stakeholder-activities.html.
  • C5.2b (4f386fa, mig.053, v1.21.0): portale esterno StakeholderPortalController (slug stakeholder-portal, non-JWT, token magic-link): access/respond/acknowledge/comment/attachment; stk_activity_responses+stk_activity_comments; allegati su evidence_files. stk-portal.html pubblica. Sotto-dashboard feedback nel dettaglio attività.

Verifica finale + hardening ✅ (commit b917d2d, mig.054, v1.21.1)

  • Flotta di verifica (Workflow, 40 agenti, 4 dimensioni + verifica avversariale a 2 lenti): 18 finding (5 major/9 minor/4 info), tutti corretti.
  • Fix principali: XSS allegati chiuso (blocklist→ALLOWLIST estensioni, no html/svg/js same-origin) + nome file random; scadenza magic-link (token_expires_at, 410) + rate-limit portale + guard scritture su attività chiuse; send() non azzera i destinatari già responded/acknowledged; assign individuale "replace"; update conserva assign_mode; editor opzioni domande a scelta; etichette stato i18n; risposte inline (no alert); ARIA; escAttr portale; voce sidebar in common.js.
  • KB SYSTEM ingerita (uuid 2c7ec47b). Cache-buster ?v=20260629, SW nis2-shell-v1.21.1.

Follow-up open-items (commit 3051983, v1.21.2)

  • ✅ Upload allegati RISOLTO. Causa: il prod non è servito dal container nis2-app ma da Apache host → php-fpm HOST (/run/php/php8.4-fpm.sock, DocumentRoot public/); public/uploads host era git:git con ACL che concedeva write solo a git, NON a www-data (utente fpm host) → move_uploaded_file falliva. Fix host (NON in repo, da ri-applicare se il dir viene ricreato): setfacl -R -m u:www-data:rwx -m d:u:www-data:rwx /var/www/nis2-agile/public/uploads. Collaudato: upload interno+portale .pdf 201, .html→422. Risolve anche evidence_files/visure (rotti per lo stesso motivo). ⚠️ Nota architetturale: il prod gira su php-fpm host (opcache.validate_timestamps=On → prende i .php in automatico); per reload immediato systemctl reload php8.4-fpm sull'host (il docker exec nis2-app kill -USR2 1 reload SOLO la dev-API del container).
  • ✅ Fonti ACN aggiornate (nis2_sources.php, verifica nis2-expert): 333017/2025 = 22 settembre 2025, marcata STORICA/superata; aggiunta 379887/2025 VIGENTE (efficace 31/12/2025); 164179/2025 (14 apr 2025) confermata vigente (base misure/requisiti). Date-giorno marcate [da confermare sul testo ufficiale].
  • 🟢 #2 — 92 vs 87 requisiti: DECISO da utente "prevale Simon" (resta 92). Aggiunta NOTA DI PROVENIENZA in nis2_sources.php (prodotto usa 92 = file referente; ACN ufficiale 87; divergenza nota/accettata). Numero NON cambiato. (Resta incoerenza con altri punti del codebase che usano 87 — accettata.)

Chiusura open-items (commit 69873e2, v1.21.3) — "fai il massimo"

  • 🟢 #7 — invio email AGGANCIATO e pronto al go-live (GATED): send() invia il magic-link via EmailService SOLO se EMAIL_SENDING_ENABLED=true. Oggi è false (default, non in .env) → zero invii (smoke prod: email_sent=0). Al go-live basta settare il flag. Link assoluto via APP_URL. I link restano sempre esposti per condivisione manuale.
  • 🟢 #8 — EMAIL_MS_URL spostato sul path interno http://172.21.0.1:8081/api/emails (host .env, fuori repo). Verificato 400 con X-Internal-Key = funzionante (pre edge-strip). Rollback: rimettere l'URL pubblico. (Inattivo finché email off.)
  • 🟢 #9 — TLS DB: DONE (sign-off VIGILE, commit d583acf). Pre-equip PHP-CA del prodotto fatto (app in TLS 1.3, cipher=TLS_AES_256_GCM_SHA384, CA application/config/db-ca.pem). Enforce REQUIRE SSL su nis2_user@'%' già attivo dall'11/6 (applicato da VIGILE nel cutover container; ssl_type=ANY) — confermato indipendentemente da NIS2-side con SHOW CREATE USER. Il mio precedente "non enforced" era falso allarme: in MySQL 8.0 SHOW GRANTS NON mostra REQUIRE (solo SHOW CREATE USER). Ciclo: richiesta OUTGOING_TO_AGILEHUB_2026_06_16 → conferma INCOMING_FROM_AGILEHUB_2026_06_16_tls_enforce_gia_attivo.md. CLAUDE.md testata → DONE.
  • 🧭 Scoperta architetturale (importante): il PROD nis2.agile.software è servito da Apache HOST → php-fpm HOST (/run/php/php8.4-fpm.sock, DocumentRoot public/, utente www-data host), NON dal container nis2-app (che serve solo la dev-API). Quindi: reload .php prod = systemctl reload php8.4-fpm sull'host (USR2 al container = solo dev-API; host ha validate_timestamps=On); public/uploads scritto da www-data host (da cui il fix ACL). Memoria: project-prod-topology-host-fpm.
  • ⏭️ #6 nginx Content-Disposition /uploads/: skip motivato (allowlist chiude la XSS; forzerebbe download dei PDF). Applicabile via restart (nginx.conf bind-mount) se richiesto.
  • 🧪 #4 collaudo E2E browser C1–C5: resta task umano dei tester. Best-effort fatto: render-smoke pagine C5 (200 + marker DOM OK). QA visivo umano comunque raccomandato.

Prossima migrazione = 055. ⚠️ Promemoria infra (non-repo, ri-applicare se ricreati): setfacl su public/uploads; EMAIL_MS_URL interno in .env.


2026-06-16 — 🟢 EPIC C (segnalazioni Simon C1–C5): C1, C2, C3, C4 LIVE — commit d370479→30f4614, v1.18.0→1.18.4

Direttive vincolanti di Simon Fattori (C1–C5), validate con l'agente nis2-expert. File sorgente autoritativi in docs/simon/ (C1_Misure_Requisiti.xlsx, C1_Vista_MisureRequisiti.xlsx, C5_EleStakeH.xlsx). 🔑 Direttiva architetturale utente: tutto data-driven via tabelle di configurazione (niente JSON hardcoded). Memoria: project-epic-c-simon.

C1 — Elenco Misure/Requisiti (config-driven) ✅

  • mig.046 cfg_nis2_ambiti(18)/cfg_nis2_misure(43)/cfg_nis2_requisiti(116)/cfg_nis2_procedure(42)/cfg_nis2_rischi(84), system-level read-only, seed da application/data/nis2_framework_seed.json (generato dai .xlsx, codifica intatta). Importer idempotente committato application/cli/seed_framework.php.
  • FrameworkController (/api/framework/catalog + /state), pagina misure-requisiti.html (2 viste, righe "spente" per classe, sola lettura). mig.047 org_requisito_state = valutazione/stato di conformità per requisito (la valutazione parte vuota → da qui emerge la conformità). mig.048 copia 42 procedure default nel modulo Procedure al 1° accesso (transazionale + FOR UPDATE).
  • Importanti 37 misure/92 requisiti, essenziali 43/116 (92 dal file di Simon, ⚠️ vs 87 contati su All.1 dall'expert — da riconciliare con Simon).

C2 — «Determinazione» (non «Determina») ✅ — decisione utente bloccata.

C3 — Rimosso modulo "Analisi GAP ACN" ✅

  • mig.049 drop acn_assessments/acn_assessment_responses (erano VUOTE, 0 dati persi). Rimossi controller/pagina/acn_measures.json/routing/sidebar/api/help/i18n. RaciController ripuntato su cfg_nis2_misure. Integrazione lg231 intatta (/services/gap-analysis usa assessments generale). Tour demo step 3 ripuntato su misure-requisiti.

C4 — Inventario a 2 voci ✅

  • mig.050 cfg_inventory_voci(2: ID.AM-01/02) + inventory_subclassi(12 default + org), assets.voce_code/subclass_id. 20 asset migrati. AssetController + assets.html (Voce+Sottoclasse, "+ aggiungi"). asset_type resta legacy.
  • 🐛 2 bug pre-esistenti risolti: apostrofo non escapato (l'inventario) rompeva il JS della pagina Inventario; common-bi.js?v=20260618 mai bumpato → la sidebar V2 non mostrava "Misure e Requisiti".

Verifica + qualità

  • Flotta di verifica (workflow 14 agenti su C1) → sicurezza pulita, fedeltà dato perfetta (codifica Simon intatta); 6 finding (1 major + 5 minor) tutti corretti (commit 45e6c35).
  • Migrazioni: prossima = 051. Help/i18n/KB aggiornati per ogni punto. Cache-buster ?v=20260625, SW nis2-shell-v1.18.4.

⏳ Resta

  • C5 — modulo Stakeholder completo (NON iniziato): 30 tipi Stak.01-30 config, anagrafica + link organigramma/procedure, doppia valutazione potere/interesse 0-5, matrice Mendelow (🔧 refuso Q4 = potere 0-2), attività/questionari+feedback collegati a procedure e misure/requisiti m2m, calendario. Da fare a fasi (C5.1 anagrafica+matrice, C5.2 attività/questionari/calendario). Mendelow = best practice, non obbligo → etichettare (fonti-certe).
  • Flotta di verifica finale + help/trad/KB: l'utente l'ha chiesta "alla fine di tutto il lavoro" (dopo C5).
  • E2E browser delle nuove UI (C1–C4): da far esercitare ai tester.

2026-06-15 (notte) — 🎉 A4 COMPLETA (5/5): RACI + Scadenziario + Stakeholder LIVE — commit b4d47d5→64866be, v1.17.2

Chiuse le ultime 3 fasi di A4 in background (workflow ultracode: scout → build backend+frontend file-disgiunti → review adversariale 3 lenti → sintesi). A4 ora completa (5/5 LIVE). Tutto pushato (HEAD 64866be), working tree pulito.

4.3 — Matrice RACI (commit b4d47d5, mig.043, v1.17.1)

  • Migration 043: raci_assignments (ruolo×oggetto; object_type ENUM inventory/procedure/risk/supplier; object_id polimorfico = NESSUNA FK, esistenza validata app-layer org-scoped in RaciController::assertObjectExists → anti-IDOR) + link m2m procedure_inventory/procedure_risk/inventory_risk/risk_measure (measure_code validato su acn_measures.json, no FK). APPLICATA su prod.
  • RaciController (route raci): matrix, assign/unassign, objects, links/link/unlink. raci.html + raci.js (griglia ruolo×oggetto, celle R/A/C/I).
  • ⚠️ Bug runner: PDO::exec rompe sui commenti -- con apostrofo → runner aggiornato a strippare le righe --.

4.4 — Scadenziario centralizzato (commit c5b00cc, mig.044)

  • Migration 044: review_schedule (entity_type role/skill/inventory/procedure/risk/supplier/measure/custom; status ok/due/overdue calcolato live). APPLICATA su prod.
  • ReviewScheduleController (route review-schedule): list/create/update/delete/complete (avanza next_review_date di frequency_months)/sync (importa scadenze sparse da policies.next_review_date / compliance_controls / risk_treatments). review-schedule.html. Ancoraggio GV.PO-02/GV.SC-07/PR.AT/DE.CM.

4.5 — Stakeholder estesi (commit c5b00cc, mig.045)

  • Migration 045: ALTER suppliers ADD stakeholder_type ENUM('supplier','customer','partner'). ⚠️ Era stata scritta con DELIMITER/stored-procedure (NON runner-safe) → review l'ha bocciata, riscritta ad ALTER bare + runner con try/catch 1060/1061. APPLICATA su prod.
  • SupplyChainController::stakeholderMap() + stakeholders.html. Riusa Supply Chain (no doppione). GV.SC-02 (fornitori + clienti + partner).

Help + i18n + KB + release

  • help.js: sezioni review-schedule + stakeholders (+ fix wording GV.SC-04/05). i18n.js: nav.review_schedule/nav.stakeholders IT/EN. Sidebar common.js.
  • KB Qdrant SYSTEM ingestata: "Scadenziario centralizzato e Mappa stakeholder (A4 4.4/4.5)" (doc_uuid 15d7b3ff).
  • Cache-buster ?v=20260620 (37 HTML). version.json → 1.17.2. SW cache bump.

Verifica

  • Smoke su prod (fpm reale): /raci/matrix, /review-schedule/list, /supply-chain/stakeholderMap → 200; pagine 200; voci sidebar presenti. Review adversariale: 0 finding critical/major (045 DELIMITER corretto in sintesi). ⚠️ E2E browser dei nuovi moduli NON fatto da me → demandato ai tester.

Email tester

  • 2 email ai 4 tester (benassati/tagliavini/fattori/mascagni, HTTP 201): (1) ringraziamento "segnalazioni Fattori accolte + aggiornato", (2) "punto del progetto" (A1/A2/A3/A4 implementate + invito a collaudare i 5 nuovi moduli via FAB). Risposte → cristiano.benassati@gmail.com.

Prossimo passo

  • Raccogliere il feedback E2E dei tester sui nuovi moduli (organigramma/competenze/RACI/scadenziario/stakeholder + onboarding). Supervisore autonomo: osservazione → implemented ~21/6. Opzionali: rotazione chiave Anthropic; PDF integrale D.Lgs. per KB; cleanup dead-code classificazione.

2026-06-15 (sera-2) — ✅ A4 Fase 4.2 IMPLEMENTATA (Competenze + gap + alert→azione) — commit 5368b0a, pushato, v1.17.0

Implementata la seconda fase di A4: le Competenze (skill, requisiti per ruolo, competenze possedute, gap, azione). Workflow ultracode: mapping in parallelo (5 reader) → implementazione → review adversariale (3 dimensioni, 0 finding) → release.

🔴 Scoperta del mapping (landmine evitato)

La tabella delle azioni NON è corrective_actions (nome nel design/memoria, INESISTENTE): è capa_actions (mig.004), figlia obbligatoria di non_conformities (NCR) via ncr_id NOT NULL, senza colonna source_type. Design doc + memoria corretti.

Backend

  • Migration 042 (docs/sql/042_competences.sql) + runner aggiornato. 4 tabelle additive/idempotenti: skills (org-owned o globali organization_id NULL), role_skills (UNIQUE role+skill), user_skills (UNIQUE org+user+skill, acquired_via_course_id), skill_course_map. APPLICATA su prod (nis2-db v8.0.45 TLSv1.3; skills 2FK, role_skills 4FK, user_skills 5FK, skill_course_map 3FK).
  • CompetenceController (route competences): catalog, skills CRUD, roleSkills/setRoleSkill/removeRoleSkill, userSkills/setUserSkill/removeUserSkill, mapCourse/unmapCourse, gapGrid (richiesto−posseduto per ruolo con titolare, corsi suggeriti, azioni aperte), openAction. Multi-tenancy + anti-IDOR + livelli 1-5; competenze globali read-only per org (solo super_admin).
  • alert→azione (openAction): riusa NCR/CAPA → crea non_conformities (source='management_review', source_entity_type='competence_gap', source_entity_id=role_skills.id, severity major se gap≥2, nis2_article=PR.AT-02 se essenziale altrimenti PR.AT-01) + capa_actions figlia, in transazione. Anti-duplicato su source_entity_id. Zero ALTER alle tabelle esistenti.

Frontend

  • competenze.html (4 tab: Catalogo · Requisiti per ruolo · Competenze persone · Gap & azioni) + js/competenze.js. CTA "Assegna corso" (riuso /training/assign, nuovo api.assignTraining) e "Apri non conformità". Bootstrap Italia V2.
  • Voce sidebar "Competenze" (common.js + common-bi.js + BI_PAGES) + nav.competences i18n IT/EN. api.js: metodi comp*.

Help + KB + PWA

  • help.js: guida 'competences' (schede, calcolo gap, PR.AT-01/02, GV.RR-04, art.24 D.Lgs., disclaimer) + mappa pagina.
  • KB Qdrant SYSTEM ingestata (doc_uuid 3739398d).
  • PWA: sw.js cache → nis2-shell-v1.17.0 (allineamento release).
  • Cache-buster ?v=20260618 dei 5 JS condivisi su 32 HTML. version.json 1.16.0 → 1.17.0.

Verifica

  • Smoke E2E su prod (fpm reale): catalogo, requisito (liv.4), competenza persona (liv.2), gap=2, openAction → NCR-766066 + CAPA-984386, anti-dup 409, mappatura corso, statici live — tutti verdi. Dati di test ripuliti dall'org tester 151 (0 residui).
  • Review adversariale (workflow, 3 dimensioni security/correttezza/fonti + verifica scettica): 0 finding.
  • Email alle attività inviata ai 4 tester (HTTP 201).

Prossimo passo A4

  • 4.3 matrice RACI (raci_assignments, ruolo×oggetto) + link m2m procedure/inventario/rischi (segn. 4/5/7). Poi 4.4 review_schedule, 4.5 stakeholder GV.SC-02.

2026-06-15 (sera) — ✅ A4 Fase 4.1 IMPLEMENTATA (Organigramma) — commit 8540b53, pushato

Implementata la prima fase di A4 (modello relazionale, docs/DESIGN_A4_RELATIONAL.md): l'Organigramma.

Backend

  • Migration 041 (docs/sql/041_org_roles.sql) + runner scripts/migrate-a4.php. Tabella org_roles additiva/idempotente: parent_role_id (gerarchia self-FK), holder_user_id (titolare), is_governance_body (organi amministrazione/direttivi, Art.23 D.Lgs.138/2024), description (GV.RR-02), sort_order, audit cols. APPLICATA su prod (container nis2-db v8.0.45, TLSv1.3, 11 col, 4 FK). ⚠️ scripts/ e docs/ NON sono montati in nis2-app (solo application/+public/): applicata con runner self-contained SQL-inline sotto application/_migrate_*_tmp.php (pattern 040), eseguito via docker exec esportando DB_ da /proc/1/environ, poi rimosso.
  • OrgRoleController.php: list (flat+tree arricchiti), get, create, update, delete, assignableUsers. Multi-tenancy su getCurrentOrgId(), anti-IDOR (id+organization_id), prevenzione cicli gerarchia (walk-up), holder = membro org, delete bloccato se ha figli (409). Route org-roles in public/index.php.

Frontend

  • public/organigramma.html + js/organigramma.js: vista ad albero (badge governance, titolare/vacante, descrizione), editor crea/modifica/elimina con select padre anti-ciclo lato UI, statistiche, "crea struttura di base". Bootstrap Italia V2.
  • Voce sidebar "Organigramma" in sezione Gestione (common.js + common-bi.js attivo + BI_PAGES), icona sitemap, nav.org_chart i18n IT/EN. api.js: metodi orgRole* (wrapper _acn).

Help + KB AI (regola "fine sessione")

  • help.js: guida contestuale 'org' (cosa rappresenta, nodo Art.23, come si usa, fonti certe D.Lgs.138/2024 art.23 + GV.RR-02 best-practice, disclaimer no-parere-legale) + mappa pagina→help.
  • KB Qdrant nis2_kb SYSTEM: ingestato doc modulo Organigramma (doc_uuid 96dbafcc…, 2 chunk). RAG verificato (search aggancia GV.RR-02 "approvate dagli organi di amministrazione").

Deploy/verifica

  • Cache-buster ?v=20260617 sui 5 JS condivisi (api/common/common-bi/i18n/help) su 32 HTML (help.js era già a 20260616 → bump obbligatorio). version.json 1.15.2 → 1.16.0 (MINOR, nuovo modulo). USR2 fatto. Smoke E2E su prod (fpm reale): login tester, CRUD, tree, anti-ciclo (422), delete-con-figli (409), cleanup — tutti verdi, org 151 ripulita.

Prossimo passo A4

  • 4.2 skills/role_skills/user_skills/skill_course_map + gap competenze (segn.3, PR.AT-01/02, GV.RR-04) + alert→azione (riuso corrective_actions).
  • Poi 4.3 matrice RACI (raci_assignments) + link m2m procedure/inventario/rischi · 4.4 scadenziario review_schedule · 4.5 stakeholder GV.SC-02.

2026-06-15 — ✅ Segnalazioni Fattori: A1/A2/A3 IMPLEMENTATE + A4 disegnato + UNICA UI + tester

Tester onboarding

  • 4 tester (Benassati, Tagliavini, Fattori, Mascagni) → password Tester2026!, role super_admin, membership org 151/152 (no wizard), login 200 verificato. Client riusabile application/cli/provision_testers.php (dry-run default, --expect guard, SSO-sync-safe), commit d50955d. Mail di ingaggio a tutti e 4 (via send-raw, con curl — python urllib bloccato da Cloudflare 1010).

Segnalazioni Simon Fattori — analisi normativa + risposta (mail → cristiano.benassati@)

  • Verifica su testi ufficiali (nis2-expert + workflow): del punto 0, alcune affermazioni SMENTITE → importanti 37/87 vs essenziali 43/116 (confermati), "requisito" è termine UFFICIALE, "Determinazione" non "Determina", inventario NON solo HW/SW, gap-ACN NON si cancella. Accolte invece: citare D.Lgs. (non Direttiva), onboarding art.3 c.9, terminologia "inventario".
  • ⚠️ Saga comma art.3: la citazione originale art. 3 c. 9 lett. b/c/d (criteri sotto-soglia) era GIUSTA; una "rettifica" intermedia (→c.13) è stata un'over-correction (inferenza incompleta prima del testo verbatim); recuperato il testo verbatim (reteambiente) → confermato c.9 b/c/d (c.13 = procedura individuazione). Salvato docs/nis2/Dlgs138_art3_ambito.md. 3 mail a Fattori (reply + rettifica + chiarifica). Lezione: verificare il testo primario PRIMA di "correggere" una citazione.

A1 — citazioni obblighi → D.Lgs. (commits 121b8c2, d009341, 9fed095)

AIService (blocco fonti iniettato + classificatore incidenti) + IncidentController + gate supervisore + help.js (~36 citazioni): obblighi citano D.Lgs. art. 23 (governance) / 24 (rischio) / 25 (notifica), Direttiva solo per principi. Verificato LIVE (help.js 53 citazioni D.Lgs., 0 residui obblighi-Direttiva).

A3 — "Inventario" non "Asset" (stessi commit)

nav + titoli + i18n IT/EN + pagine assets (titoli, label, stringhe JS modali/notifiche) + guida help.js → "Inventario/beni". Tabella assets/endpoint/asset_type (tecnici) invariati.

A2 — onboarding classificazione (commits b2f47c0, c5fce7b, 717f9ac)

  • Migration 040 (11 colonne organizations, applicata+verificata prod): bilancio, autonomia, size_independent_category, 3 crit_*, mission, codice etico, designation_basis, self_assessment_status.
  • Nis2ClassificationService v2 (nuovo, centralizzato → eliminata duplicazione Onboarding/Organization): matrice settore×dimensione + bilancio €43M, size-independent (art.3 c.5), criteri sotto-soglia (art.3 c.9 b/c/d → sub_threshold_candidate, NIENTE auto-classe), obblighi D.Lgs. art.23/24/25, esito self_assessment_status=preliminary + disclaimer ACN (Det.333017, DPR445, finestra 1/1-28/2). 9/9 unit + smoke HTTP.
  • UI wizard onboarding esteso (step2 nuovi campi + step4 3 domande/obblighi/disclaimer/badge "preliminare"), classificazione v2 lato server, markup Bootstrap Italia/AGID. JS valido, campi serviti. E2E browser del wizard = da far esercitare ai tester.

UNICA UI (commit 3c5184b)

Rimossi 28 duplicati *-bi.html (orfani post-swap, 0 riferimenti). Le pagine ufficiali (nomi senza -bi) SONO la UI V2 Bootstrap Italia. Verificato: ufficiali 200 / -bi 404 / sidebar 0-ref.

A4 — DISEGNATO (commit dbda3ef), NON implementato

docs/DESIGN_A4_RELATIONAL.md: organigramma org_roles (+ nodo governance Art.23) + matrice RACI (raci_assignments) + skill/gap-competenze/formazione + m2m procedure/inventario/rischi/misure + scadenziario centralizzato (review_schedule). 5 fasi (4.1 organigramma → 4.5 stakeholder GV.SC-02). Riuso training/policies/assets/risks/suppliers/capa; isms_roles resta per SoA SGSI.

Aperti / prossimi

  • A4 da implementare (fasi 4.1→4.5) — epic multi-tabella + UI.
  • Help/traduzioni/KB AI da aggiornare per A1/A2/A3 (regola CLAUDE.md impatti funzionali). [in corso a fine sessione 15/6]
  • Dead-code innocuo: getClassificationExplanation/getClassificationDetails.
  • PDF integrale D.Lgs. 138/2024 per KB completa (non auto-fetchabile da GU/Normattiva, serve manuale).

2026-06-14 — ✅ SUPERVISORE AUTONOMO TICKET LIVE (cron) + chiusura demo/avatar/feedback

PWA conforme AGID — v1.15.0 (sessione serale)

NIS2 reso PWA installabile su Android (Chrome) e iOS/iPadOS (Safari "Aggiungi a Home"), responsive, conforme AGID/WCAG 2.1 AA. Avviato come flotta di agenti in background, completato inline dopo riavvio del processo (gli agenti background erano andati persi).

  • File nuovi: public/manifest.webmanifest (theme #0066CC blu Italia, display standalone, orientation any, shortcuts Dashboard/Incidenti) · public/sw.js · public/offline.html · public/js/pwa.js (register SW + banner installazione Android + hint iOS, i18n IT/EN) · public/assets/icons/* (icon-192/512 + maskable + apple-touch 180 + favicon 16/32 + icon.svg master) · scripts/gen-pwa-icons.mjs (generatore icone zero-deps, pure Node+zlib) · scripts/inject-pwa-head.mjs (iniezione idempotente tag head).
  • Service worker: cache nis2-shell-v1.15.0; /api/* network-only (no leak cross-utente su device condiviso), navigazioni HTML network-first → offline.html, asset stale-while-revalidate. Bumpare il nome cache ad ogni release.
  • Head: tag PWA iniettati in 77 HTML (64 + 6 admin + 7 subdir, escluso offline.html); viewport normalizzato a viewport-fit=cover mantenendo lo zoom (WCAG 1.4.4).
  • AGID/WCAG: Bootstrap Italia (già live), zoom+orientamento liberi, touch target ≥44px (@media (pointer: coarse) in style.css), safe-area in standalone. "Dichiarazione di accessibilità" obbligatoria solo per PA → opzionale, non implementata.
  • Rigenerare: node scripts/gen-pwa-icons.mjs (icone) · node scripts/inject-pwa-head.mjs (tag head, idempotente). Tutto statico → live via bind-mount, nessun USR2.
  • Bump version.json 1.14.1 → 1.15.0. Committato 36513fa (21:09 del 14/6) e pushato su Gitea (b80bf23..36513fa, 00:38 del 15/6, via host con chiave fresca). Verificato end-to-end: manifest JSON valido + campi installabilità, sw.js/pwa.js sintassi OK, 70 pagine servite con manifest+pwa.js (0 gap), artefatti live 200 su prod (manifest/sw/offline/icone), AGID OK (0 pagine bloccano lo zoom, touch-target ≥44px, safe-area standalone, icone maskable).

Supervisore autonomo ticket (lavoro principale di oggi)

NIS2 = primo adopter LIVE dello standard AgileHub autonomous-supervisor-agent (hub_standards id=32). Un agente claude -p --model opus headless (chiave SSH effimera per ciclo) fa da supervisore end-to-end ai ticket NIS2 → NIS2 esce dal ticket-agent-cron generico.

  • Cron LIVE: /etc/cron.d/nis2-supervisor → 17 */4 * * * (ogni 4h, minuto 17). Kill switch: touch /etc/agilehub/supervisor-nis2.disabled. Rollback: rm /etc/cron.d/nis2-supervisor. Audit: /var/log/nis2-supervisor-audit.jsonl.
  • Prompt: operativo scripts/nis2-supervisor-acting-prompt.md (+ mie 2 aggiunte obbligatorie: cache-buster ?v= su fix JS/CSS su tutti gli HTML referenti · git backup/commit chirurgico) + gate normativo docs/nis2-supervisor-prompt.md ("fonti certe": no chiusura senza fonte citata, Allegato 3=importanti/4=essenziali, 24h/72h/1mese, DB SELECT-only container TLS, release edit+USR2 no maintenance, escala legale).
  • Validazione (2 run osservate, non-cron): run #1 ticket #366 (domanda normativa notifiche) → PASS gate (24h/72h/1 mese, Allegato 3/4 corretti, fonti Dir.2022/2555+D.Lgs.138/2024 Art.25+ACN, [DA VERIFICARE] per comma non nel registro, 0 scritture DB/codice). run #2 ticket #367 (fix: rimossa icona FontAwesome morta fa-comment-alt in help.js:770, residuo post-swap V2) → PASS release path, commit dcbce3d (37 file: 35 HTML ?v=20260613→20260614 + help.js + version.json 1.14.1, 0 file backend/DB, fix live).
  • Doc archivio: docs/INCOMING_FROM_AGILEHUB_2026_06_14_supervisor_adoption.md → commit 489a032 (verificato su Gitea via ls-remote).
  • Rotazione chiave Anthropic = RINVIATA (decisione VIGILE/AgileHub, risk-accepted: TTL breve + re-rotazione prevista + accesso protetto a monte + nessun segnale abuso). Trigger override monitorati (breach/IP inatteso/chiave in canale pubblico/scadenza senza re-rotazione). Verificato NIS2-side: la chiave esposta NON è in repo/git-history/.env (solo vault cifrato tier1__nis2-app__anthropic) → nessun trigger scattato. Il cron supervisore usa l'OAuth della flotta, non queste API key. Memoria: project_autonomous_supervisor.

Cron supervisore — confermato che parte DA SOLO (giri sera 14/6 → notte 15/6)

  • 4 cicli rc=0 no-op verificati nell'audit JSONL: 16:17 / 20:17 / 00:17 (+ run osservate #366/#367). Ogni giro: pre-check OK (no kill switch, no agent-working.lock), chiave effimera installata→cycle_cleanup_done, 4 code tenant 7 vuote, nessuna release, heartbeat consegnato a cristiano.benassati@ (HTTP 201). HEAD non cambia ai giri (no-op).
  • Chi-ha-fatto-cosa: il commit 36513fa (PWA, 21:09) NON è del supervisore — nell'audit non c'è alcun ciclo alle 21:09 (giri solo a :17). Era lavoro interattivo mio. Dubbio chiuso.
  • Monitoraggio de-escalato (concordato con utente): il supervisore è indipendente (cron server + chiave effimera propria per ciclo + heartbeat al referente) → il mio check host-side è un livello bonus, non una dipendenza di sicurezza. Check all'occasione, niente poll ansioso.
  • ⚠️ Lezione operativa — la chiave SSH host del MIO monitoraggio ruota ~14h: scaduta tra le 20:27 e le 00:16 (push + check host bloccati in quella finestra), AgileHub ne ha droppata una fresca in /projects/nis2-agile/.ssh-temp/agilehub-host-temp (00:38) → ripristinato. NB: è cosa diversa dalla chiave effimera che il supervisore si genera da sé per ciclo (quella non dipende da me).
  • (TRPG, non NIS2) db-job 20: poller AgileHub — fuori scope NIS2.

Lavoro 13/6 (avatar/formazione + feedback) — dettaglio in memoria, qui sintesi

  • Avatar di prodotto FORMAZIONE-first: 5 endpoint /api/demo/*, dataset demo 996001 (RO) + 996002 (sandbox), guard applyDemoGuard (read 200/write 403), manifest tour nis2-tour-2026.json (10 step), frontend demo-mode (?demo=), tassonomia 10mod/28lez + banca esercizi. Collaudo E2E PASS sul subdomain. Memoria: project_demo_avatar. Handoff: docs/OUTGOING_TO_AGILEHUB_2026_06_13.
  • Regressioni Bootstrap Italia V2 fixate: FAB icone FA→SVG inline · conflitto .modal BI (showModal invisibili) → override CSS · widget bug-reporter 401 → cablata X-API-Key · cache-buster ?v= introdotto (JS/CSS erano senza versioning) · feedback consolidato sullo standard TRPG (rimosso auto-inject feedback.js, tenuto solo bug-reporter). Memoria: project_bootstrap_italia_rollout.
  • Demo dataset "golden": 2 org coerenti (DataCore importante + MedClinic essenziale). Account reali preservati. Memoria: project_demo_dataset.

Aperti / prossimi

  • Osservazione supervisore 3 giorni → hub_standards_adoption[id=32, nis2] = implemented ~21/6 (passiva, kill switch a portata).
  • Backlog/debito: cache headers server-side (evita i bump manuali ?v=) · token widget a basso privilegio (AgileHub-side).
  • ✅ docs/sql/039_integrity_keys.sql ora committato+pushato (in b80bf23, su Gitea).

2026-06-12 — ✅ AUDIT INTEGRITÀ DB + migrazione 039 (PK/UNIQUE/FK)

Task utente: "controlla se tutte le tabelle hanno le chiavi e le unique key corrette" → poi "procedi a sistema db" (dati demo, ok ad applicare).

Diagnosi (read-only, su DB reale prod)

  • Sonda PHP via information_schema lanciata dentro nis2-app (l'unico che raggiunge il container DB 172.21.0.4 con la connessione TLS dell'app). Devenv NON raggiunge il DB → SSH host 172.18.0.1 (chiave .ssh-temp/host_key) + docker exec -i nis2-app sh -c 'set -a; eval "$(tr "\0" "\n" < /proc/1/environ | grep -E ^DB_)"; set +a; php /dev/stdin' (le creds DB sono vault-injected nel PID 1, NON nell'env di docker exec). Pattern riutilizzabile. ⚠️ La prima esecuzione con export $(... | xargs -d) ha stampato l'intero env (segreti) in output — busybox non ha xargs -d; usare la forma eval qui sopra che NON stampa.
  • Esito: 65 tabelle base, 65/65 con PRIMARY KEY, 65/65 InnoDB (nessuna anomalia bloccante).
  • TABLE_ROWS di information_schema è approssimato (consulting_firms dava ~0 ma la firm 1 esiste) → i check orfani/duplicati usano COUNT() reale.

Sistemato (migrazione 039, applicata su prod — 18 vincoli, 0 errori)

Runner idempotente PHP con pre-check duplicati/orfani/tipi. File: docs/sql/039_integrity_keys.sql (NON committato ancora).

  • 🗑️ Drop indice UNIQUE ridondante invites.token_hash (duplicava uq_invites_token).
  • 🔑 3 UNIQUE su chiavi naturali (0 duplicati reali): api_keys.key_hash, refresh_tokens.token, consulting_firms.vat_number.
  • 🔗 14 FK mancanti stesso-DB: policy_attestations(×3), policy_versions(×2), org_acn_requirement_status(×2), supplier_questionnaires(×2), org_connectors, control_evidence_auto, firm_org_assignments.consulting_firm_id, kri(×2). ON DELETE: organization_id/padre→CASCADE, link nullable (kri.linked_risk_id)→SET NULL. DB ora a 109 FK totali.

Differiti (NON applicati — richiedono decisione)

  1. supplier_categories.organization_id=0 (10 righe) = sentinel "categoria globale di sistema" (seed mig.033) → FK sarebbe scorretta per design. Lasciato.
  2. firm_org_assignments.organization_id → org 126/127/128/129 inesistenti (dogfooding "Agile" firm 1, anche assigned_to=0). Per la FK serve prima: seedare le org 126-129 oppure DELETE delle 9 righe stale, poi ADD CONSTRAINT fk_firm_org_assignments_organization_id (DDL pronto in fondo al file 039).

Aperti / prossimi passi

  • Commit + push di docs/sql/039_integrity_keys.sql su Gitea (atteso ok utente; push via host/VIGILE — token devenv bloccato).
  • Decidere caso firm_org_assignments orfano (seed vs delete) → poi chiudere l'ultima FK.

2026-06-11 — ✅ CUTOVER TLS-DB FATTO: NIS2 su container nis2-db (TCP+TLS), anomalia socket chiusa

Dopo 3 outage notturni (TLS-su-socket), risolto definitivo con lo standard "come TRPG":

  • VIGILE ha migrato host→container nis2-db (copia esatta 60 tab/19 utenti/max_id 392; host intatto, backup /root/backups/nis2-migration-20260611/).
  • Io: .env DB_HOST=172.21.0.4 (IP del container — fpm NON risolve db) + reload fpm (docker exec nis2-app kill -USR2 1) → TCP verde (login reale 401, container, 19 utenti). Poi touch application/config/.db_ssl_on (+ CA container già giusta) + reload → Ssl_version=TLSv1.3 dal worker reale, login Fattori 200, ARIA RAG ok → TLS verde.
  • VIGILE: ALTER USER 'nis2_user'@'%' REQUIRE SSL sul container → post-check verde: plain no-SSL rifiutato (1045), app 200/ok.
  • Guard anti-outage in Database::sslOptions() (commit 4548cc6): salta SSL su connessione socket → riaccendere il flag su socket non può più causare outage.
  • Tecnica usata senza SSH continuo: edit file bind-mounted (.env posseduto da uid 1000, application/config/, public/) + diag serviti da public/ via HTTPS + reload fpm via SSH host. Regola d'oro confermata: verificare SEMPRE dal worker fpm reale (login), MAI da CLI/docker exec/api-status (CLI dava falsi positivi: DB_HOST e source-IP divergono).
  • File d'ambiente (.env, .db_ssl_on, db-ca.pem) sono gitignored (corretto). Memoria project_db_topology aggiornata (ora: container via TCP+TLS).
  • ⏭️ Residuo VIGILE: decommission host (DROP nis2_user@localhost) quando confermata stabilità → NIS2 self-contained come TRPG.

2026-06-10 — TLS-DB pre-equip (VIGILE) + SCOPERTA split DB host/container

Task da CLAUDE.md (cima, "VIGILE 2026-06-10"): pre-equip PHP-PDO/CA per TLS-in-transito DB, gated default-OFF, senza enforce (l'ALTER USER è di VIGILE). Eseguito con precisione chirurgica + 2 agenti background di review.

Fatto (commit 4d89b1e)

  • application/config/database.php → Database::sslOptions(): TLS gated (env DB_SSL=true o flag-file application/config/.db_ssl_on), DEFAULT OFF. PDO::MYSQL_ATTR_SSL_CA + VERIFY_SERVER_CERT=false. Fail-safe: CA assente → resta in chiaro + error_log (un VERIFY=false senza CA in mysqlnd connette IN CHIARO silenziosamente — doc §11). Merge con + (preserva chiavi-intere PDO, §14).
  • Verificato locale (no DB): php -l + reflection test → OFF ⇒ base + [] === base (inerzia provata, zero regressione); ON+CA-mancante ⇒ []; ON+CA ⇒ chiavi 1009/1014.
  • 2 agenti background: (a) censimento connessioni → runtime application/ coperto 100% da Database::getInstance() (0 raw PDO); 9 new PDO raw solo in NON-prod (simulate-*.php come root → ininfluenti; scripts/import-feedback-to-nexus.php:112/114 cross-DB e public/test-runner.php:529 da gestire prima dell'enforce SE usano nis2_user). (b) review adversariale → SICURO default-OFF, nessun bug.
  • .gitignore: application/config/.db_ssl_on + db-ca.pem. Runbook completo: docs/TLS_DB_PREEQUIP_NIS2.md.

🔴 SCOPERTA CRITICA: split DB host vs container (confermata da VIGILE)

  • La recon (docker exec nis2-app php -r 'Database::getInstance...') ha rivelato: l'app nis2 usa il DB CONTAINER (DB_HOST=db → 172.21.0.4, server fd63ec13e889, MySQL 8.0.45), NON il MySQL host (8.0.46). Questo CORREGGE la memoria project_db_topology (diceva "host"). Ssl_version=[] (chiaro), nis2_user@%.
  • VIGILE ha confermato lo split + fixato: il cron sso-password-sync aggiornava le copie host MORTE di nis2_agile_db (+ lg231/wms) mentre le app usano i container → password SSO non arrivavano. Fixato (sync sul container reale via docker exec). taxai resta su host (la sua app usa davvero l'host). trpg/sustainai intatti.
  • Conseguenza sul mio lavoro 2026-05-29: migrazioni 020/021/022 erano state applicate via mysql -h localhost = copia host morta → mancanti nel container → v1.7.0 NON funzionava in prod. VIGILE le ha ri-applicate al container (backup+idempotenti) → verificato assets.relevance_score/incidents.nis2_incident_type/incident_pir ✅. Schema v1.7.0 ora corretto in prod.
  • ⚠️ Per il TLS: CA + ALTER USER nis2_user@% REQUIRE SSL vanno sul CONTAINER db (non host). Comandi corretti nel runbook.

Aperti (in mano a VIGILE — DB = solo Agile)

  1. Benassati/org129: assente nel container (host id=4, SSO #232); membership org129=0 sia host sia container. Stato voluto comunicato: super_admin (NO declassare) + org_admin membro org 129 nel CONTAINER, SSO-linked. NB add-agile-user.php crea i nuovi come org_admin → NON usarlo as-is per lui; probabile gap provisioning SSO nel container. VIGILE inserisce da contesto DB_HOST=db.
  2. Riconciliazione migrazioni 023→036: applicate (sessioni 05-30/05-31) via mysql -h localhost = path host → da verificare/ri-applicare sul container come 020-022. Sentinelle per-migrazione fornite a VIGILE (tabella docs/TLS_DB_PREEQUIP_NIS2.md + chat). Offerto script SQL unico di reconciliation (information_schema) — da generare se richiesto.
  3. Enforce TLS: di VIGILE, con 3-sì, dopo CA distribuito sul container + verifica sul path reale (Ssl_version=TLSv1.3, lezione §16 "no test sintetici").

Accesso

Chiave SSH effimera in .ssh-temp/ (rigenerata dall'utente, validità ~3h). Devenv NON raggiunge il DB prod direttamente: solo via SSH host (root@172.18.0.1 o 135.181.149.254). Push Gitea bloccato sul token (git-login interattivo) — commit locali, push a cura host/VIGILE.

🔴🔴 AGGIORNAMENTO SERA — la "scoperta container" era SBAGLIATA; verità: fpm→HOST via SOCKET

Le conclusioni "container DB" qui sopra sono superate. Verificato col path fpm reale (diag PDO servito da php-fpm, NON docker exec che è CLI):

  • L'app HTTP (php-fpm/www-data) usa il MySQL HOST dfm-dev (8.0.46) via UNIX SOCKET (localhost, env non propagato ai worker). Lo schema v1.7.0 è presente sul host (le migrazioni 020/021/022 del 29/5 via mysql -h localhost erano sul DB GIUSTO). Il docker exec nis2-app php (DB_HOST=db→container 8.0.45) è solo il path CLI, secondario. Memoria project_db_topology ri-corretta (fpm→host socket; CLI→container).
  • OUTAGE causato + risolto: VIGILE ha attivato CA+flag+REQUIRE SSL (sul container) → ma TLS su UNIX socket non si applica e ROMPE ([2002] Cannot connect to MySQL using SSL) → app giù (login 500). Fix: rm application/config/.db_ssl_on + kill -USR2 1 → ripristino. VIGILE poi: NIS2 🔵 escluso dall'enforce (caso socket), container revertito a baseline, app verde.
  • Tentativo switch socket→TCP (per allinearlo agli altri): VIGILE ha aperto un path TCP via vault-net (172.30.0.1, host MySQL già in ascolto lì) + grant nis2_user@'172.30.%'. MA il path fpm reale FALLISCE: prova affiancata 3+3 → CLI (root) getsockname=172.30.0.9 → nis2_user@172.30.0.9 OK; fpm (www-data) getsockname=**172.30.0.1** → MySQL vede 135.181.149.254 (IP pubblico) → 1045. Causa probabile: policy-routing/NAT per-uid (www-data instradato via gateway). Il collaudo "OK" di VIGILE era CLI = falso positivo. → switch NON eseguito (sarebbe outage). Palla a VIGILE: grant @'135.181.149.254' (quick) o fix routing per-uid; poi ri-probo da fpm prima di switchare.
  • Stato finale: app verde (socket clear), pre-equip TLS inerte (flag OFF), db-ca.pem presente ma inerte. Patch codice committato (4d89b1e), corretto e pronto; non attivabile finché l'app è su socket / il TCP fpm non è grantabile.
  • ⚠️ Lezione ricorrente della sera: validare SEMPRE sul path fpm reale (diag servito da php-fpm: login/business), mai docker exec/CLI né /health — CLI e fpm divergono (DB_HOST, source IP, opcache).

2026-06-01 — Modulo Gap Analysis ACN + review + landing (prod v1.13.0, ahead 0)

⚠️ EMAIL ancora DISABILITATE (EMAIL_SENDING_ENABLED=false, solo dati demo). Vedi sezione 2026-05-31 + memoria project_email_killswitch.

Origine: finding revisore — la Gap Analysis Art.21 (10 lettere) NON è l'autovalutazione ACN reale (misure/requisiti puntuali). Verifica su fonti ACN in docs/ANALISI_GAP_ANALYSIS_ACN.md (revisore aveva ragione).

NUOVO modulo Gap Analysis ACN (Determinazione 164179/2025, assessment 2° livello):

  • IMPORTANTI 37 misure/87 requisiti, ESSENZIALI 43/116. Filtra per organizations.entity_type.
  • Dataset canonico application/data/acn_measures.json (testi INTEGRALI da Allegati 1/2, flag per-requisito importante/essenziale, validato 0 discrepanze).
  • AcnAssessmentController.php (catalog/list/create/get/requirements/respond/complete/report/aiAnalyze), scoring per funzione FW (GV/ID/PR/DE/RS/RC), anti-IDOR. Migrazione 036 (acn_assessments+acn_assessment_responses) APPLICATA su host.
  • public/acn-gap.html + voce sidebar + metodi api.acn*. Gate: guida cap-5b, help 'acn', i18n IT/EN, version 1.13.0. E2E verificato in prod (catalog 87, scoring esatto).

Review avversariale 4 agenti → 5 bug fixati (commit d6924a3):

  • CRITICO aiAnalyze trattava return array di askWithRag come stringa → ora estrae 'answer'.
  • ALTO: acn_requirements.json (sorgente ingest Qdrant) aveva 188/203 testi TRONCATI → rigenerato integrale + ri-ingest (chunk 458-1052 char). NB: container nis2-app monta SOLO /application e /public → per ingest serve docker cp script+json in /tmp + symlink /application.
  • MEDIO catalog() entity_level=null → 422. MEDIO guida cap-5 "figure chiave organigramma" → "personale autorizzato + sysadmin". BASSI: openAcn try/catch, badge IT/EN, overall_score null se tutti N/A.

Landing/presentation (commerciale):

  • public/index.html aggiornata alle reali funzionalità (8→12 moduli: Gap Analysis ACN in evidenza, Gap Analysis Art.21, Portale Fornitori, AI Consulente RAG, KB multi-livello), hero stats (116 req ACN, 12 moduli, RAG), meta description, trust-items.
  • Rimossi TUTTI i riferimenti a costi/gratuità (richiesta utente "sembra gratis"): index CTA ("nessuna carta di credito"→"accesso controllato"), presentation.html slide 9 ("Modello commerciale/abbonamento annuale"→"Edizioni") + contatti ("Pricing"→"Attivazione su richiesta").
  • FIX bug layout consenso GDPR nel form: .form-group input{width:100%} gonfiava il checkbox → testo incolonnato verticale + tutto rosso. Fix classe .consent-label (checkbox 16px, span flex, colore normale). Verificato con render headless Chrome. Causa vera trovata solo al 3° tentativo.

Memorie aggiornate: project_acn_gap_analysis (nuova), project_email_killswitch (precedente). docs/ANALISI_GAP_ANALYSIS_ACN.md committato.


2026-05-31 (sera) — Fasi 2-4 questionari fornitori + corpus ACN + KILL-SWITCH EMAIL (prod, ahead 0)

⚠️ EMAIL DISABILITATE — ambiente con SOLI dati demo. EMAIL_SENDING_ENABLED=false (fail-safe). NESSUNA mail parte. Riattivare SOLO al go-live reale e SOLO dopo aver rimosso i dati demo. Vedi sotto + memoria project_email_killswitch.

Kill-switch email (commit 397185e, ultimo lavoro della sessione)

  • Guard EMAIL_SENDING_ENABLED in config.php (default false SEMPRE, anche con APP_ENV=production che qui è di fatto demo) all'inizio di EmailService::sendViaRelay() E sendViaTemplate() — gli unici 2 punti che fanno HTTP al relay → coprono TUTTI i canali (incidenti, training, inviti, reminder, welcome, password reset, feedback, OTP portale, cron Fase 2).
  • Verificato a runtime nel container: EMAIL_SENDING_ENABLED=false, send()→false, sendViaTemplate()→false (nessuna chiamata rete).
  • Contesto scoperto: INTERNAL_EMAIL_KEY=nexus-internal-2026 valorizzata + APP_ENV=production nel .env host → senza switch le mail SAREBBERO partite davvero.
  • Riattivazione go-live: EMAIL_SENDING_ENABLED=true via env/vault + kill -USR2 1.

Fix collaterale (commit 006f863)

  • EmailService::sendViaTemplate() era MANCANTE (Edit silenziosamente fallito in de09af6): requestOtp() chiamava un metodo inesistente → OTP mai inviato (errore inghiottito dal try/catch). Aggiunto davvero + verificato method_exists a runtime. Payload usa data (campo canonico relay AgileHub) + alias vars.

Modulo questionari fornitori al 100%. Tutto committato e pushato.

Fase 2 — campagne + automazioni: backend campagne+cruscotto (7baa596) già pronto; scripts/supplier-questionnaire-runner.php + .sh (261fc4c) — 3 fasi idempotenti (REMINDER su reminder_offsets, OVERDUE due_at<NOW→expired, RICORRENZA clona con token sq_), dry-run host OK. AZIONE UTENTE: registrare crontab 0 6 * * * via agile-services.

Fase 3 — portale OTP/magic-link: SupplierPortalController.php (de09af6) auth SEPARATA (SUPPLIER_JWT_SECRET, aud=supplier-portal, supplier_sessions revocabili; OTP 8 cifre lockout persistente; sendViaTemplate fuori da email_log; request-otp opaco; no-IDOR). public/supplier-portal.html (78dcb41) — E2E verificato in prod (verify-otp→token→/me→getQuestionnaire 26 domande→PATCH autosave), poi cleanup. Config: SUPPLIER_JWT_SECRET + PATCH in CORS. Migrazioni 034+035 applicate.

Fase 4: tutti i tipi domanda nel portale + scoring per-vulnerabilità (Art.21.3) in submitQuestionnaire.

Corpus AI: scripts/ingest-acn-requirements.php (6365d5d) — 203 requisiti ACN in Qdrant nis2_kb (entity_type=requisito_acn), verificato count=203.

Gate (2dced3f): guida cap-9 + help.js + version 1.12.0.

Residue (non bloccanti dev): crontab; go-live (chiave mktg, APP_ENV).


2026-05-31 — Modulo Questionari Fornitori (Fase 0+1) + doppia review 22 agenti (HEAD 43a7180, ahead 0, prod v1.11.0)

Ambiente: SVILUPPO. Le 2 azioni di sicurezza (ruotare chiave API mktg nel DB, impostare APP_ENV=production) sono RIMANDATE — non servono ora, da rifare prima di un go-live reale (decisione utente).

COSA È STATO FATTO

  • Design modulo questionari fornitori + portale OTP + AI consulente: 5 review + 4 deliverable (template GV.SC, DDL, UX mini-spec, AI consulente). Doc: docs/DESIGN_MODULO_QUESTIONARI_FORNITORI.md + docs/supplier-portal/. Commit aa2db4c.
  • Fase 0 (email relay): EmailService::send() ora instrada via relay AgileHub /api/emails/send-raw (X-Internal-Key, env multi-source, maskEmail). Fixa bug prod mail(). Smoke E2E = SENT. Commit 5457611.
  • Fase 1 (modulo questionari configurabile):
    • Migrazioni 032+033 APPLICATE su host (mysql -h localhost): 6 tabelle nuove + 10 categorie preset (org 0) + suppliers.category_id/external_ref/source. 034/035 NON applicate (Fase 2/3).
    • Backend SupplyChainController: CRUD categorie/template/domande + import CSV/API (bulkUpsert per external_ref). Route in index.php.
    • Seed template "NIS2 base" = 26 domande GV.SC su org 129 (Agile Technology), mappate ad Allegato 2 ACN con nis2_ref + disclaimer interpretazioni.
    • UI supply-chain.html: dropdown categoria nel form + modali Categorie (CRUD) e Template (read-only domande con badge nis2_ref) + import CSV con errors[]. i18n sp.* IT/EN.
  • 2 REVIEW MULTI-AGENTE (12 + 10 = 22 agenti) su tutta l'app/help/i18n/guida/AI. Bug reali fixati:
    • CRITICI: KB token-key errata + init mancante (8a55194); register() senza jti → utente buttato fuori + revoca sessione singola 404 (c134a2d); incidents.html SyntaxError apostrofi (a92e27c, sessione precedente).
    • MEDI: dashboard gauge 0% (fallback score), risks backToList 4-viste + loadFair data.items (2037cec); selettore modale #app-modal→#modal-overlay (4d9153b); deleteAsset this.delete→del (d103e9d).
    • NORMATIVA: Allegato 3=IMPORTANTI / 4=ESSENZIALI corretto in AIService+help+IncidentController (era invertito); relazione finale = notifica+1mese (Art.23.4.d) non rilevazione+30gg; disclaimer non-parere-legale in guida+AI (authoritativeSourcesBlock regole 4-5); citazioni GV.SC con [Interpretazione] dove serve.
    • AI: crossOrgAnalysis +fonti certe, suggestRisks anonimizzato (employeeRange), EmbedService/VectorService dim 512.
    • SECURITY F1: chiave API mktg nis2_mktg_8c8bd38e... (scope admin:licenses) era ESPOSTA in mktg-api-doc.html + integrazioniext.html → redatta a placeholder (e269fb1). DA RUOTARE nel DB (resta in git history) prima del go-live.
  • Rifinitura help: aggiunto help contestuale ai 4 moduli che ne erano privi (whistleblowing, normative, kb, companies) → ora tutti i 16 moduli hanno il pulsante "?". Commit c1d3328 + 3e5b75b + 43a7180.

ESITO REVIEW: app SOLIDA — 0 SQLi/IDOR/XSS cross-tenant, JWT+refresh atomico, DB integro (0 orfani, 26 domande org 129, 10 categorie), 0 citazioni normative inventate.

TODO RESIDUI (non bloccanti, vedi memoria project_supplier_portal):

  • i18n EN: dizionario 278 chiavi ESISTE ma non cablato nei markup → app di fatto solo-IT (decisione utente: lasciare, IT è lingua primaria).
  • Collation drift LOW su tabelle PRE-esistenti invites + incident_pir (utf8mb4_0900_ai_ci) — non tocca il modulo fornitori, NON ancora sistemato (richiede ALTER, chiedere conferma).
  • Cut-over SupplyChainController da supplier_questionnaires(027) a questionnaire_campaigns da fare in lockstep con migrazione 035 (Fase 2/3).
  • Per-account login lockout (oggi solo per-IP).
  • AI/KB grounding: D.Lgs.138/2024 assente dalla KB (file=null), 203 requisiti ACN solo in DB non in Qdrant, Determina 164179 sotto-estratta (9 chunk) — vedi docs/supplier-portal/AI_CONSULENTE_NORMATIVO.md per il piano.
  • 2 azioni go-live (rimandate, ambiente sviluppo): ruotare chiave API mktg; APP_ENV=production su Hetzner.

2026-05-30 (sera) — Guida allineata + email invito

  • guida.html allineata alle 10 nuove funzionalità (commit 5e2534e FAIR/KRI/benchmark + 397d181 ingestion SIEM, attestation+versioning policy, self-assessment fornitori, import CMDB, monitoraggio continuo, connettori per-azienda). Stile coerente "in parole semplici/esempio/norma". Àncore cap-1..cap-15 intatte (help.js continua a linkare i capitoli giusti via _guideAnchor). HTML bilanciato. HEAD 397d181, ahead 0.
  • Email invito guida inviata (HTTP 202, queued) a m.tagliavini + s.fattori + presidenza @agile.software, firma "Cristiano Benassati", reply_to cristiano.benassati@gmail.com.
    • ⚠️ Lezione email: l'invio NON funziona con EmailService::send (mail() nativa → sendmail nel container rifiuta, 127.0.0.1:25 connection refused) né con Authorization: Bearer. Il canale CORRETTO è il relay AgileHub: POST https://agilehub.agile.software/api/emails/send con header X-Internal-Key: <INTERNAL_EMAIL_KEY> (in .env, keylen 20), body JSON {to,subject,html,product:'nis2',reply_to,priority}. Doc canonico: docs/STANDARD_EMAIL_RELAY.md. Postfix diretto è bloccato per tutti i prodotti.

2026-05-30 — Risoluzione bug residui post-review (HEAD 8d1f0f9, ahead 0)

Dopo i fix dei 🔴 (vedi sotto), risolti anche i ⚠️ di correttezza-dati segnalati dai 5 agenti:

  • a3f8211 Bug #1+#4 supply chain: submitPublicQuestionnaire ora coerente con assessSupplier (risk_score=compliance alto=buono, security_requirements_met soglia 70, NON sovrascrive criticality) + completamento ATOMICO (UPDATE ... WHERE status='sent' + rowCount()==0→409). E2E: 201 poi 409.
  • 9c64ae7 Bug #2 computeFair: validazione range (TEF≤1M, LM≤1000mld, no negativi/NaN, vuln≤1) → no overflow DECIMAL(16,2). E2E: valido 200, overflow/vuln>1/negativo→422.
  • 4e8a90c Bug #3 sectorBenchmark: dedup pool con subquery correlata (ultimo completato, tie-break id, LIMIT 1) → una riga per org anche con timestamp identici. E2E: org con 2 assessment stesso TS → peers=4 (non 5). NB: primo tentativo (Edit fallito su ancora) committato vuoto, fix reale in 4e8a90c.
  • ⚠️ Falso allarme mio: ho erroneamente segnalato "prompt-injection" negli output tool — verificato che NON esisteva (grep repo vuoto, nessun hook in settings). Ritrattato. Nessuna esfiltrazione richiesta/eseguita.
  • Tutti gli 8 controller lint-clean. Nota workflow ricorrente: gli Edit falliscono se l'ancora non combacia → SEMPRE Read del file reale prima, e verifica esito (grep/curl) prima del commit.

SESSIONE 2026-05-30 (gap competitivi P2/P3 + UI) — riepilogo finale

Stato git: origin/main = 172d927, ahead 0. Tutto su Gitea.

Completato e VERIFICATO E2E in produzione

  • 4 feature P1/P2 backend + UI (sessione mattina): ingestion incidenti, evidence automation, asset import, FAIR/KRI. UI in risks/reports/assets + help/i18n/OpenAPI/scorecard EVIX.
  • P2 Benchmark settoriale anonimizzato: GET /dashboard/sectorBenchmark (k-anonymity ≥3 org) + UI pannello dashboard. 200 OK.
  • P3 Supply chain self-assessment: migrazione 027, SupplyChainController (sendQuestionnaire JWT + publicQuestionnaire/submitPublicQuestionnaire NO-auth token + questionnaireStatus) + public/supplier-assessment.html. E2E: send 201→publicGET 200 (8 domande)→submit 201 (score 61)→re-submit 409→suppliers.risk_score aggiornato.
  • P3 Policy attestation+versioning: migrazione 028, PolicyController (attest/attestations/versions/diff/pendingAttestations + snapshot in approve). E2E: approve→attest(coverage)→bump v2.0→diff(+2/-1)→pending ricompare.

⚠️ LEZIONE RICORRENTE (grave, ripetuta più volte oggi)

Le Edit falliscono silenziosamente quando l'ancora non combacia col file reale, e ho committato senza ri-verificare via HTTP → 3 commit P2/P3 erano gusci vuoti (migrazione+HTML ma metodi/route mancanti → 501/404). Corretti con commit successivi (31b8a45, 172d927) DOPO smoke test. Regola d'oro: dopo ogni feature, curl l'endpoint reale PRIMA del commit; non fidarsi del "Edit success". Inoltre Edit richiede Read del file nella sessione corrente.

🔌 Connettori per-azienda nella card (RICHIESTA UTENTE, in sospeso — decisione necessaria)

Utente vuole: "le credenziali si configurano nella card per ogni azienda cliente" → card in companies.html (consulente), storage segreti scelto vault-steward. Finding (verificato sull'host): vault-steward (server.js 2123 righe) e nis2-vault-proxy esistono; nis2-app ha VAULT_APP_TOKEN scoped tier1__nis2-app__* e fetcha al boot (READ). NON risulta un'API runtime che l'app possa usare per SCRIVERE nuovi segreti: il populate è operazione ADMIN via docker exec vault-steward node scripts/vault-repopulate.js + register-app (CLI), non esposta al token applicativo. VAULT_STEWARD.md non montato nel devenv. → DECISO e IMPLEMENTATO (opzione A — commit 6c8f8e2): config in DB, secret nel vault via CLI.

  • Migrazione 029: tabella org_connectors (organization_id, connector_type ENUM 8 tipi, display_name, enabled, config JSON NON-segreto, vault_key_alias, secret_status, last_status). NESSUN segreto nel DB.
  • OrganizationController: listConnectors/saveConnector/deleteConnector + connectorOrgGuard (org_admin/compliance_manager della propria org, o firm che la gestisce, o super_admin). Difesa secret-strip: client_secret/api_key/password/private_key/token inviati vengono rimossi prima del save (verificato E2E: NON entrano nel DB). saveConnector ritorna cli_hint col comando vault-cli.js migrate <alias> <key> <value>.
  • Route organizations/{id}/connectors GET/PUT/DELETE (type nel body).
  • UI: pannello "Connettori" nella card di companies.html (8 tipi, tenant/client id, toggle attivo, badge stato segreto, modal). Servito 200, JS valido.
  • Vault write-path confermato assente per l'app: server.js (615 righe) espone solo GET /v1/credentials/* + tier2 unlock/lock; scrivere segreti è solo CLI admin (vault-cli.js migrate). Doc: /opt/devenv/VAULT_STEWARD.md.
  • E2E in prod: list vuota 200 → save m365 201 (secret strippato) → DB pulito → delete OK.

Stato git fine sessione: origin/main = 6c8f8e2, ahead 0. Tutto su Gitea.

REVIEW MULTI-AGENTE (5 agenti) + FIX — fine sessione 2026-05-30

Lanciati 5 agenti read-only di review (P1, P2, P3, connettori, frontend). Esito: nessuna vuln critica nelle feature, ma trovati problemi reali. Tutti corretti, testati E2E, pushati. Stato finale: origin/main = dac41f8, ahead 0.

  • 🔴 8e3a2c1 risks.html: apostrofi non escapati in LIKELIHOOD_DETAILS (bug PRE-ESISTENTE dal 2026-02-20) rompevano l'INTERO script della pagina Rischi (FAIR/KRI inclusi). Fix + verificato LIVE.
  • 🔴 9f4d2a1 Connettori (SEC): (1) connectorOrgGuard usava users.role globale invece del ruolo per-org → feature ROTTA per utenti reali; ora ancorata al route {id} + user_organizations.role. (2) secret-strip denylist→allowlist ricorsiva. E2E: globale=employee+per-org=org_admin→200; non-membro→403; 11 varianti segreti→DB solo {account_id,region}.
  • 🟠 c7e1f04 Policy: migrazione 030 UNIQUE(policy_id,version) + approve() ON DUPLICATE KEY UPDATE (no snapshot duplicati); diff set-based→LCS posizionale. E2E ok.
  • 🟠 b2e9c33 P1 ingestion: retry su collisione incident_code + race external_ref→200 dedup (no più 500=alert perso); try/catch su CCM services.
  • 🟡 dac41f8 P2: relevance_criteria uniformato (score() salva criteri piatti come bulkUpsert).
  • ✅ Falso allarme agente P1: getNistCsfMapping/getIsoMapping ESISTONO (verificato).
  • Migrazioni totali sessione: 023-030. ⚠️ Le 027/028/029 NON erano state pushate come gusci? No: tutte applicate in prod + codice completo dopo i fix.

SESSIONE 2026-05-30 mattina (~07:50→09:15 CEST) — Chiusura gap competitivi Evix (backend)

Obiettivo utente: "NIS2 punto di riferimento" → implementare DAVVERO le carenze vs concorrenza (Vanta/Drata/GRC) dall'analisi docs/EVIX_ANALISI_CONCORRENZA.html. 4 feature backend complete, testate E2E in prod, committate e pushate.

Feature implementate (tutte su Services API + scope dedicati)

  1. P1 Ingestion incidenti SIEM/SOC/EDR (commit 2190999): POST /api/services/incidents-ingest (scope ingest:incidents). Crea incidente Art.23 da alert esterno, dedup su external_ref, mapSeverity (CVSS/P1-P5/stringhe→enum), classificazione AI (IS-1..4), deadline 24h/72h/30g, webhook. Migrazione 023 (incidents += source/source_system/external_ref + UNIQUE dedup).
  2. P1 Evidence Automation + Continuous Control Monitoring (commit 307993f): POST /api/services/evidence-ingest (scope ingest:evidence, batch ≤200) + GET /api/services/controls-monitoring (scope read:compliance). Stato semaforo healthy/warning/stale/failing per freschezza. Migrazione 024 (tabella control_evidence_auto + compliance_controls += monitoring_status/last_checked_at/freshness_days).
  3. P2 Asset import CMDB/cloud (commit 4924075): POST /api/assets/import (JWT) + POST /api/services/assets-ingest (scope ingest:assets). AssetScoringService::inferCriteria (euristica 6 criteri GV.OC-04 da campi CMDB) → scoring automatico. Upsert dedup su external_ref, max 1000/batch. Migrazione 025 (assets += external_ref/discovery_source + UNIQUE).
  4. P2 Risk quantitativo FAIR + KRI (commit a3f2e91): FairService Monte Carlo (PERT su TEF/Loss Magnitude, ALE EUR P10/P50/P90, deterministico). POST /risks/{id}/fair, GET /risks/fairRegister (portfolio ALE), KRI CRUD GET/POST /risks/kri + PUT /risks/kri/{id} con semaforo green/amber/red. Migrazione 026 (risks += params FAIR/ALE; tabella kri).

UI area provider (commit 094d453)

public/integrazioniext.html: sezione "Catalogo Connettori — Evidence Automation" con 8 card "In roadmap" (M365/Google/AWS/Azure/IdP/EDR/SIEM/Ticketing) + badge .badge-roadmap.

API key create (org 129 Agile, prod — salvate dall'utente)

SIEM nis2_siem_70246bbf..., lg231 nis2_lg231_2b6b4c09..., SustainAI nis2_sustai_30fb3796..., AllRisk nis2_allrsk_ecb9ea92..., Evidence nis2_evid_4cc44366..., CMDB nis2_cmdb_ebbc0dc8.... (key_prefix = varchar(12)! created_by NOT NULL → uso 330.)

⚠️ LEZIONI CRITICHE (cambiano il workflow)

  • 🔴 OPcache validate_timestamps=Off: le modifiche PHP NON sono live finché non si fa docker exec nis2-app sh -c 'kill -USR2 1' (reload graceful). Il bind-mount aggiorna il file ma php-fpm serve il bytecode in cache.
  • 🔴 Modifiche non committate a RISCHIO: durante la sessione un commit esterno (d5d83bb, cron agent/operatore) ha fatto checkout e revertato la mia route in index.php non committata. → committare+pushare appena una feature è verde.
  • ✅ Push su Gitea: fatto via SSH dall'host (git push con helper vault), il container devenv non ha il token.

✅ Completamento UI + contorni (commit 8a2f4d1, pushato)

  • UI frontend delle 4 feature (ora usabili dagli utenti, non solo via API):
    • risks.html: viste "Quantitativo (FAIR)" (form + istogramma ALE + registro portafoglio) e "KRI" (dashboard semafori + CRUD)
    • reports.html: tab "Monitoraggio Continuo" (semafori freschezza controlli + copertura)
    • assets.html: bottone "Importa" CSV/CMDB (parsing client + scoring auto GV.OC-04)
    • api.js: metodi computeFair/getFairRegister/listKri/createKri/updateKri/importAssets/getControlsMonitoring
  • help.js: guide FAIR+KRI / import CMDB / monitoraggio continuo. i18n.js: chiavi IT/EN.
  • OpenAPI /api/services/openapi: aggiunti incidents-ingest/evidence-ingest/assets-ingest/controls-monitoring + ApiKeyAuth.
  • AuditController::controlsMonitoring (versione JWT per UI) + route audit/controlsMonitoring.
  • EVIX scorecard: gap P1/P2 marcati chiusi (backend), roadmap P1/P2 con voci ✅ FATTO.
  • Verificato in prod: OpenAPI espone i 4 endpoint, pagine 200, import JWT 201, monitoring JWT 200.

✅ Gap P2/P3 rimanenti — IMPLEMENTATI (sessione 2026-05-30, pomeriggio)

  • P2 Benchmark settoriale anonimizzato (commit a673033): GET /dashboard/sectorBenchmark — confronta score org vs aggregati anonimi del settore (avg/median/p25/p75/percentile/posizione), k-anonymity ≥3 org. UI: pannello dashboard con barra distribuzione. Differenziatore rete multi-tenant. Testato E2E (peers=4, avg/median esatti).
  • P3 Supply chain self-assessment (commit 8f4e8e9): migrazione 027 (tabella supplier_questionnaires), SupplyChainController::sendQuestionnaire (JWT, link token 30gg) + publicQuestionnaire/submitPublicQuestionnaire (NO auth, token) + questionnaireStatus. 8 domande Art.21.2.d pesate → score → suppliers.risk_score. Pagina pubblica public/supplier-assessment.html (standalone). Pulito route map (rimossi duplicati + PueT garbage + metodi inesistenti). Testato E2E (send→public GET→submit→dedup 409→bad token 404).
  • P3 Policy attestation + versioning (commit 75de1c2): migrazione 028 (policy_versions + policy_attestations), approve() crea snapshot versione, attest/attestations(copertura %)/pendingAttestations/versions/diff?from&to. Version-aware: bump versione → richiede ri-attestazione (verificato E2E: diff added/removed corretto, pending ricompare dopo v2.0).

⚠️ Trovato file PRE-CORROTTO (non toccato)

public/supply-chain.html è corrotto in HEAD da prima (committato 2025-09-29): righe garbage the ▐ }, const file = list.clea; // BUG, funzioni duplicate. NON è di questa sessione. La UI admin per inviare questionari fornitori va aggiunta dopo aver riparato questa pagina (task separato). Il backend + portale pubblico P3 supply chain funzionano comunque via API.

❌ Restano (prossimi passi reali)

  • Riparare public/supply-chain.html (pre-corrotto) + aggiungere UI admin "Invia questionario" / stato attestation policy / lista versioni+diff.
  • Connettori per-vendor live (OAuth M365/Google/AWS/Azure/IdP/EDR): le 8 card sono "In roadmap"; servono client + credenziali/app-registration lato cliente.
  • Auto-discovery attiva asset (agent/scan) — l'import bulk e gia disponibile.
  • Gap P2 Reporting/benchmark settoriale e P3 Supply chain/policy non ancora affrontati.
  • NB workflow: OPcache validate_timestamps=Off → dopo ogni modifica PHP serve docker exec nis2-app sh -c 'kill -USR2 1'. Le modifiche a HTML/JS sono servite da nginx senza reload.

SESSIONE POMERIGGIO — 2026-05-29 (~15:30→17:10 CEST)

0. Perché "tutti i prompt sono morti improvvisamente"

Il container nis2-agile-devenv si è RIAVVIATO alle 15:25:55 CEST. Prova: uptime (up 4 min) + supervisord.log (supervisord ripartito da pid 98 alle 15:27, nessun crash/respawn interno prima) → restart esterno (probabile reboot VM Hetzner; docker/dmesg non ispezionabili da dentro). Le sessioni Claude girano come processi DENTRO il container → terminate tutte insieme. Nessuna perdita dati (bind mount persistente). Il cron ticket-agent è ripartito normalmente (OPEN_TICKETS.md riscritto alle 15:30 → conferma ripresa, benigno). Niente OOM/disco pieno.

1. Messa in sicurezza sorgenti (git)

HEAD era già = origin/main (4a85abe), ma c'era MOLTO lavoro non committato — incl. l'intero modulo RAG/KB di aprile mai entrato in git. Creati 5 commit su main (⚠️ SOLO LOCALI, NON ancora su Gitea):

  • 1d13166 [CHORE] .gitignore: esclude *.bak*, .backups/, .ssh-temp/ (quest'ultimo proteggeva una chiave SSH privata!)
  • c0bf7b6 [DOCS] standard cross-suite + governance CLAUDE.md + registri agent
  • 1d934e4 [FEAT] UI: guida.html, index-en, mobile-conversion, ai-assistant, bug-reporter, help/i18n
  • 9b53ca3 [FEAT] MktgLead getJsonBody + import-feedback-to-nexus + seed demo
  • a7a21fa [FEAT] Knowledge Base RAG (KnowledgeBaseController + VectorService/EmbedService/RagService + kb.html/js + SQL 012/013 + AIService::askWithRag + qdrant in compose)
  • 🔐 Scrub sicurezza: rimossa la API key Voyage hardcoded da EmbedService.php e docker-compose.yml (ora solo in .env gitignored + vault). NON è finita su Gitea. (Valutare rotazione della chiave: era in chiaro su disco, condivisa con sustainai.)
  • Rimossi 2 duplicati orfani: application/controllers/{config.php,EmailService.php} (copie identiche dei canonici).

⚠️ TODO CRITICO: i 5 commit sono solo nel .git locale. Per il backup su Gitea serve git-login (token, cache svuotata dal reboot) + git push origin main. Il push tentato è fallito per assenza token. Anche questo file (CONTEXT) e altre eventuali modifiche doc sono da committare/pushare.

2. Modifiche DB PRODUZIONE — utenti/org "Agile"

Scoperto che "Agile" = consulting_firms id 1 → "Agile Technology SRL" (unico studio nel sistema). Il super_admin vive in users.role (enum globale), NON in user_organizations.role.

  • ✅ Creato utente Simon Fattori (id 330, s.fattori@agile.software): role=org_admin, consulting_firm_id=1, utente LOCALE (no SSO, sso_identity_id=NULL), password Fattori2026!@ impostata e verificata con password_verify.
  • ✅ Tagliavini (id 326) declassato super_admin → org_admin (resta firm 1). role NON è sincronizzato dall'SSO → cambio stabile. Restano altri super_admin (3 admin@certisource, 4 benassati, 46 worker, 107 sim-b2b).
  • ✅ Creata organizzazione Agile Technology SRL (id 129, entity_type=important, sector=ict_services, consulting_firm_id=1, voluntary_compliance=0) per dogfooding NIS2 su sé stessi. org_admin: Fattori (is_primary) + Tagliavini. firm_org_assignments 7/8/9. Firm 1 ora gestisce 126/127/128/129.
  • employee_count e annual_turnover_eur lasciati NULL → da completare in onboarding (servono per soglia dimensionale NIS2).
  • Tutte le scritture in transazione (commit OK), solo su nis2_agile_db.

Accesso infrastruttura (IMPORTANTE per le prossime sessioni)

  • ❌ Dal container devenv NON si raggiunge il DB di produzione (nis2_user@172.18.0.7 → Access denied; localhost non ha MySQL qui).
  • ❌ La chiave SSH documentata docs/credentials/hetzner_key è ASSENTE.
  • ✅ Usata la chiave SSH effimera .ssh-temp/id_ed25519_nis2-agile_8h_20260529-120834 (validità 8h da 12:08 → scade ~20:08 del 29/05) per ssh root@135.181.149.254 → mysql -h localhost -u nis2_user. Creds DB da /var/www/nis2-agile/.env sul server.

3. Integrazione analisi docs/nis2/ → prodotto (v1.7.0) — DEPLOYATA in produzione

L'utente ha caricato in docs/nis2/ mockup HTML di un sistema NIS2 + 5 PDF normativi ufficiali. Integrati nel prodotto e deployati live. Doc completo: docs/nis2/INTEGRAZIONE_COMPLETATA.md.

  • Fase 1 Asset Relevance Scoring NIS2 0-100 a 6 criteri (GV.OC-04): AssetScoringService.php, endpoint assets/scoringGrid|{id}/score|relevantSystems, UI assets.html, registro stampabile (audit/relevantSystemsRegister). SQL 020.
  • Fase 2 Tassonomia incidenti Determina ACN 164179/2025: IS-1..4 + entity_obligation essential/important (Allegati 3/4). SQL 021, IncidentController::create, AIService::classifyIncident.
  • Fase 3 PIR 5-Whys + metriche TTD/TTC/TTR + timestamp di fase auto. SQL 022 (tab incident_pir), endpoint incidents/{id}/metrics|pir.
  • Fase 4 Mapping NIST CSF 2.0 (43 controlli) reference-only: audit/nistCsfMapping (no migrazione).
  • FONTI CERTE (richiesta utente: AI+help citano fonti certe): application/config/nis2_sources.php (registry), AIService::authoritativeSourcesBlock() iniettato nei prompt (vieta riferimenti inventati), citazioni in help.js, ingest PDF normativi nella KB: scripts/ingest-nis2-sources.php → 287 chunk in Qdrant nis2_kb scope SYSTEM (retrieval verificato).
  • Analisi concorrenza Evix: docs/EVIX_ANALISI_CONCORRENZA.html (gap-driven, suite=prodotti Agile, vs GRC intl + NIS2 IT).

Eseguito su Hetzner: backup /root/backup_pre_v170_20260529_165447.sql; migrazioni 020/021/022 applicate; ingest 287 chunk; smoke test endpoint (401 ok).

⚙️ Scoperte infrastrutturali NON OVVIE (per prossime sessioni)

  • 🔑 /projects/nis2-agile (devenv) e /var/www/nis2-agile (host) sono LO STESSO filesystem via bind mount → le modifiche ai file sono già live in produzione, scp/git-pull NON servono. Confermato: stesso git status.
  • 🐛 Qdrant IP drift: nis2-qdrant non ha IP statico in compose, era driftato 172.21.0.5→172.21.0.3. L'IP era hardcoded in VectorService.php E in docker-compose.yml (QDRANT_URL). Fix applicato: entrambi → hostname http://nis2-qdrant:6333 (drift-proof). app ricreato (docker compose up -d app), vault/chiavi preservate, RAG web verificata.
  • ⚠️ docker.conf ha clear_env = no → php-fpm EREDITA le env (contraddice la vecchia nota CLAUDE.md su clear_env): per questo l'IP morto in QDRANT_URL rompeva la RAG web. CLI risolve l'hostname Qdrant via Docker DNS.
  • ℹ️ pdftotext presente sull'host ma NON nel container nis2-app (alpine); Qdrant raggiungibile solo dal container (network), non dall'host. Strategia ingest: estrarre testo su host in cache .txt, far girare l'ingest nel container (legge .txt).
  • ✅ ARIA cablato: creato AiController::ask (POST /api/ai/ask) → AIService::askWithRag. Il FAB ARIA (common.js) chiamava /api/ai/ask ma il controller NON esisteva → assistente AI era rotto, ora funziona e cita le fonti certe (verificato in prod: rag_used=True, sources = Ambiti/Determina). kb_uploaded_documents non esiste su questo DB (mig. KB 012-014 non applicate qui) → tracking MySQL KB saltato (best-effort), ma la ricerca legge da Qdrant.
  • 🐛🔑 DNS php-fpm (verificato con diagnostico fpm-fcgi reale, poi rimosso): nei worker php-fpm Alpine/musl, durante una request HTTP, getenv('QDRANT_URL') ritorna false E gethostbyname/curl NON risolvono gli hostname Docker single-label (nis2-qdrant → "Could not resolve host"). Funziona SOLO un IP letterale (172.21.0.3 → 200). In CLI invece getenv+gethostbyname funzionano. Perciò: VectorService fallback = IP letterale 172.21.0.3 (fpm-safe, live via bind mount); QDRANT_URL compose = hostname (per CLI/cron, drift-proof via gethostbyname). ⚠️ DRIFT: se nis2-qdrant viene ricreato e cambia IP, aggiornare il fallback in VectorService.php. Fix definitivo (non fatto, richiede recreate rete): ipv4_address statico per qdrant in docker-compose.yml (subnet 172.21.0.0/16; IP liberi es. .10). NB: clear_env=no in docker.conf NON basta a passare l'env ai worker (verificato getenv=false).
  • ℹ️ Esiste un public/_dbg_voyage.php (debug Voyage, non mio) ancora presente in prod — valutare rimozione.

⚠️ TODO aperti (sessione 3)

  • PUSH Gitea: 2 commit miei (5c545ea feat + 94d7867 fix Qdrant) + i 5 dell'agente sono SOLO locali. Serve git-login (token) + git push origin main. Tentato, fallito per token assente.
  • Lavoro "prossimo" da AgileHub: l'utente l'ha menzionato ma senza allegare contenuto/ID ticket → da chiarire alla ripresa.
  • Opzionale: cablare askWithRag a endpoint web; valutare IP statico Qdrant in compose; applicare migrazioni KB kb_uploaded_documents se serve la lista doc in UI.

SESSIONE MATTINA — 2026-05-29 (TRPG alignment)

Data: 2026-05-29 Durata: sessione molto lunga — progetto allineamento NIS2↔TRPG completato


Cosa abbiamo fatto

Progetto allineamento NIS2 ↔ TRPG (suite Evix)

Doc canonico: docs/GAP_TRPG_NIS2_ALIGNMENT.md

Analisi gap tra TRPG v1.54.1 e NIS2 v1.0.0 → 18 gap (G01-G18) raggruppati in 5 fasi. 6 decisioni utente confermate (§10 del doc). Tutte le 5 fasi completate in unica sessione. Version 1.0.0 → 1.5.0.

Fase 1 — SSO Federation (v1.1.0)

  • Migration 015_sso_columns.sql: aggiunge users.sso_identity_id, users.password_version
  • Nuovo application/services/SsoHelper.php — client SSO dual-mode (cURL nativo, zero deps)
  • AuthController::login() + changePassword() con conditional SSO (dietro SSO_MODE)
  • .env su Hetzner + vault-steward tier1__nis2-app__sso/internal_key (placeholder)
  • AgileHub Ticket #220 aperto a team AGILEHUB per estendere sso-password-sync.sh
  • SSO_MODE=local di default → comportamento utente invariato

Fase 2 — Multi-device Sessions (v1.2.0)

  • Migration 016_active_sessions.sql: tabella active_sessions (jti tracking) + refresh_tokens.session_jti
  • BaseController::requireAuth() verifica jti + last_activity throttle
  • BaseController::parseDeviceLabel($ua) — parsing UA-friendly
  • login() genera jti + insert active_sessions, logout() revoca selettiva, changePassword() revoca altre sessioni
  • 3 nuovi endpoint: GET/DELETE /auth/sessions[/{id}]
  • UI settings.html tab Sicurezza: card "Sessioni Attive" con device list + revoca

Fase 3 — Password Reset + Context Switch (v1.3.0)

  • Migration 017_password_reset.sql: tabella password_reset_tokens (TTL 30 min, single-use)
  • Endpoint POST /auth/forgot-password (risposta opaca anti-enumeration) + POST /auth/reset-password
  • Pagine HTML: forgot-password.html, reset-password.html (con strength bar)
  • login.html: link "Password dimenticata?" ora funzionante (era alert manuale)
  • EmailService::sendPasswordReset() aggiunto
  • Endpoint POST /auth/switchContext con rotazione JWT (revoca old session + nuovo jti + organization_id claim)
  • Dropdown tenant in sidebar esposto a TUTTI gli utenti con ≥2 org (prima solo consulenti)
  • _switchOrg() in common.js ora chiama switchContext + setTokens

Fase 4 — Impersonate + Preferences + Versioning UI (v1.4.0)

  • Endpoint POST /auth/impersonate (super_admin o consulente stesso firm, TTL 1h, JWT con impersonated_by claim, audit log)
  • Migration 018_user_preferences.sql: users.theme/timezone/notif_email/notif_inapp
  • Endpoint GET/PUT /auth/preferences
  • Sidebar footer mostra versione corrente, click → modal changelog
  • common.js _loadVersionFooter() fetcha /version.json al boot

Fase 5 — Branding + Auth-gate (v1.5.0)

  • Migration 019_firm_branding.sql: tabella firm_branding (logo/colori/brand name per consulting firm)
  • BrandingController.php (NUOVO): GET /branding/current (auth opzionale), PUT /branding (super_admin o consulente)
  • common.js _loadFirmBranding() applica CSS variables al boot
  • public/js/auth-gate.js copiato/adattato da TRPG (gate password client-side per documenti riservati)
  • G15 skip: simulator esistenti coprono demo flows
  • G18 skip: refactor controller rinviato (~5gg investimento, valore tecnico)

Scoperta CRITICA durante Fase 3: topologia DB

Vedi MEMORY.md → project_db_topology.md per dettagli.

PHP-FPM nel container nis2-app (clear_env=yes + .env mancante in container) ricade su DB_HOST=localhost di default → connette al MySQL del HOST Hetzner, NON al container nis2-db.

Conseguenza: tutte le migration vanno applicate via:

mysql -u$DB_USER -p$DB_PASS -h localhost $DB_NAME < migration.sql

dal HOST Hetzner, NON docker exec nis2-db mysql.

Lo si è scoperto perché Fase 3 ha fatto 500 con "table password_reset_tokens doesn't exist" anche se la migration era stata applicata "con successo" — andava sul DB sbagliato.


SQL (5 migration nuove, tutte applicate al DB host):

  • docs/sql/015_sso_columns.sql
  • docs/sql/016_active_sessions.sql
  • docs/sql/017_password_reset.sql
  • docs/sql/018_user_preferences.sql
  • docs/sql/019_firm_branding.sql

PHP nuovi:

  • application/services/SsoHelper.php
  • application/controllers/BrandingController.php

PHP modificati:

  • application/controllers/AuthController.php (+13 metodi)
  • application/controllers/BaseController.php (requireAuth + parseDeviceLabel + generateRefreshToken)
  • application/services/EmailService.php (+sendPasswordReset)
  • application/config/config.php (+2 costanti)
  • public/index.php (+12 route + branding controller)

HTML/JS:

  • public/login.html (link forgot-password)
  • public/forgot-password.html (NUOVO)
  • public/reset-password.html (NUOVO)
  • public/settings.html (tab Sicurezza con sessions)
  • public/js/common.js (version footer + branding loader + tenant switcher esposto)
  • public/js/auth-gate.js (NUOVO)
  • public/version.json (1.0.0 → 1.5.0)

Doc:

  • docs/GAP_TRPG_NIS2_ALIGNMENT.md (NUOVO, piano completo + stato esecuzione)

Backups creati (tutti con timestamp 20260529-*):

  • /projects/nis2-agile/application/controllers/*.bak.pre-{sso,sessions,fase3,fase4}-*
  • /projects/nis2-agile/public/*.bak.pre-{sessions,fase3,fase4}-*
  • Hetzner /var/www/nis2-agile/.backups/*

File deployati su Hetzner

Tutti via scp -i .ssh-temp/id_ed25519_nis2-agile_8h_*:

  • 4 PHP in /var/www/nis2-agile/application/controllers/
  • 1 PHP in /var/www/nis2-agile/application/services/
  • 1 PHP in /var/www/nis2-agile/application/config/
  • 5 HTML/JSON in /var/www/nis2-agile/public/
  • 2 JS in /var/www/nis2-agile/public/js/

Nessun docker restart eseguito (non necessario, bind mount + PHP-FPM rilegge ad ogni request).


Decisioni utente confermate (§10 doc)

  1. SSO_MODE iniziale: local (switch a dual dopo ≥7gg validazione)
  2. Backfill SSO: lazy on-login (no script massivo)
  3. Sessioni concorrenti: nessun limite
  4. TTL token reset: 30 min
  5. Cadenza version bump: MINOR per fase (eseguito 1.1.0 → 1.5.0)
  6. Branding white-label: mantenuto in Fase 5 (eseguito)

Problemi aperti / dipendenze esterne

  • AgileHub Ticket #220 (priorità per switch a dual): estendere sso-password-sync.sh per includere DB nis2_agile_db. Status: OPEN, dispatchGroup=RESOLVER, dispatchProduct=AGILEHUB.
  • vault placeholder tier1__nis2-app__sso/internal_key: contiene PLACEHOLDER_REGISTER_WITH_TENANT_MS_BEFORE_ACTIVATING_DUAL_MODE. Va sostituito con chiave reale ottenuta dal Tenant MS prima del switch.
  • docker compose up -d --force-recreate app richiesto per attivare env vault SSO_INTERNAL_KEY quando si passa a dual (oggi opzionale, defaults sani lato codice).
  • SSO_MODE=local → dual: modifica una sola riga in .env. Da eseguire solo dopo aver risolto le 3 dipendenze sopra. Bumpare version dopo (oltre 1.5.0).

Prossimi passi consigliati

  1. Validare 1.5.0 in produzione con il primo utente reale (test password reset + sessions UI + tenant switcher)
  2. Attendere risoluzione AgileHub Ticket #220
  3. Ottenere SSO_INTERNAL_KEY dal Tenant MS
  4. Replace placeholder vault: docker exec vault-steward node /app/cli/vault-cli.js migrate tier1__nis2-app__sso internal_key '<real>'
  5. Cambiare .env su Hetzner: SSO_MODE=local → SSO_MODE=dual
  6. docker compose up -d --force-recreate app per ricaricare env
  7. Smoke test login utente con identità SSO esistente
  8. Bumpare a 1.6.0 con changelog "SSO dual mode attivato"

File chiave da sapere

  • CLAUDE.md — single source of truth governance
  • docs/GAP_TRPG_NIS2_ALIGNMENT.md — piano 5 fasi + stato esecuzione
  • MEMORY.md (auto) → project_db_topology.md — lezione CRITICA su DB host vs container
  • MEMORY.md (auto) → project_alignment_trpg.md — stato progetto allineamento