Files
nis2-agile/docs/CONTEXT_LAST_SESSION.md
T

903 lines
111 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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-07-30 — 🟢 Gestione Rischi 12 colonne (integrazione prototipo "Modulo Azioni" di Simon) LIVE v1.26.0
Memorie: [[project_modulo_azioni]], [[project_ticket_500_risks_page]], [[project_session_release_mgmt]], [[project_cruscotto_studio]].
**Contesto in arrivo:** Simon ha inviato un prototipo HTML ampio ("Modulo Azioni": ciclo miglioramento azioni↔audit↔rischi/policy/KPI↔RACI↔budget↔calendario) + spec tabella Rischi a 12 colonne + 2 bug/domande. Richiesta Cristiano: analizza, confronta con agenti normativi, integra con le eccezioni motivate, aggiorna help/KB, manuale, invia a Simon.
**A. Analisi (3 agenti):** Explore (mappatura codebase: KPI/Budget assenti=greenfield; `capa_actions` figlia-di-NC=attrito; `risks` ha già inerente+residuo ma non sulla vista requisiti; calendario read-only; "RISCHIO(DEFAULT)"=`cfg_nis2_rischi.risk_descr`) + **nis2-expert** + **iso-27001-expert**. Verdetto: impianto valido; 🔴 col.12 (residuo→conformità requisito)=confusione 2 piani; "preventiva" rimossa ISO 2013; banda 13-14 scoperta; "Danno €"→Impatto; soglie=metodo interno non obbligo.
**B. FASE 1 IMPLEMENTATA E LIVE (v1.26.0):** tabella `public/risks.html` a **12 colonne** (inerente+residuo, editing INLINE P/I 1-5 auto-save, Alert a bande→popup+crea azione, codice→Misure e Requisiti). Backend: **mig 067** (`org_requisito_state += residual_likelihood/impact`; tabella `requisito_actions`), `RiskController::derivedList`(+`risk_descr`+residuo)/`setState`(+residuo)/`requisitoActions` list+create. Verificato puppeteer (sandbox 996002). **7 eccezioni normative** applicate (la #1: residuo NON auto-aggiorna conformità → flag Esito residuo + stato esplicito). Commit `8a0bc5c`+`ea57928`+`371e98c`.
**C. Contorno:** help.js+i18n IT/EN+KB `RISCHI_MODELLO.md` (re-ingest via `application/_ingest-risks.php` DENTRO nis2-app — host non raggiunge Qdrant 172.21.0.5) + manuale `docs/MANUALE_GESTIONE_RISCHI_12COL.md`. **Email a Simon** (integrazione+7 eccezioni+manuale, 201) + nota ticket #501 (201). Release v1.26.0 (version.json+SW cache).
**D. Altri due punti Simon (stessa sessione):** parere normativo su all-hazard #501 p.3 (mantenere, mai rimuovere — obbligo art.21.2/art.24) inviato ticket+email; bug «Le mie aziende»→dashboard = **cache stale** (Simon 330 super_admin, companies.html carica OK live; navigazioni SW=network-first→reload risolve). ⚠️ design da confermare: admin studio con role=org_admin+cf_id NON vedono il Cruscotto (gate solo consultant/super_admin).
**Gotcha sessione:** prod=host php-fpm (`systemctl reload php8.4-fpm`, oltre USR2 nis2-app); ticket-ms message endpoint richiede header **`X-Tenant-Id: 7`**; `git add -A` fallisce su doc INCOMING di altro utente→add esplicito. **Roadmap:** Modulo Azioni completo (registro azioni/audit bidir/KPI/budget/calendario) = fasi 2-5, attesa conferma eccezioni da Simon. Prossima mig=**068**.
## 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](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:
```bash
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.
---
## File creati/modificati (riepilogo)
**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