Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
883 lines
107 KiB
Markdown
883 lines
107 KiB
Markdown
# Contesto Ultima Sessione
|
||
|
||
> Il 2026-05-29 ci sono state DUE sessioni: **pomeriggio** e **mattina** (TRPG). Il 2026-05-30 sessione lunga: gap competitivi P1/P2/P3 + connettori + review multi-agente + fix.
|
||
|
||
## 2026-06-27 — 🟢 UI V3 (revamp top-nav) LIVE + allineamento help/i18n/KB/ARIA + mail tester + test completo + polish
|
||
|
||
Memorie: [[project-ui-v3-rollout]], [[project_aria_chat_wiring]], [[project-bootstrap-italia-rollout]] (superato).
|
||
|
||
### A. Revamp UI V3 (top-nav navy/oro, Inter+Lucide self-host) — LIVE
|
||
Sostituita la sidebar Bootstrap Italia (V2) con una **top-nav a mega-menu**. Flusso: manutenzione flag-gated (`.htaccess` 503 + `maintenance.html`, bypass `?mnt=`) → FASE 0 foundation (`public/css/v3.css`, `public/js/topnav-v3.js`, `public/vendor/inter|lucide`, dashboard pilota) commit `b0fc190` → flotta di agenti su **32 pagine app** (incl. admin/*) commit `a4150b5` → verifica (200/no-CDN/no-residui-V2) → rimossa manutenzione. `v3.css` ha un layer **widget compat** (modal/toast/form/btn-variant/spinner) + utility (`.btn`/`.text-muted`/`.sr-only`) + alias var V2→V3 così `common.js` funziona senza `style.css`. **Reskin** (wiring pagina preservato, id/data-* intatti). Escluse standalone: supplier-assessment/simulate*/service-continuity. ⚠️ Manutenzione durata ~5h (vs ~1h stimata) per riavvii del processo che uccidevano gli agenti di sfondo (i file scritti però persistono).
|
||
|
||
### B. Allineamento help / traduzioni / KB / ARIA al top-nav — commit `58b4bdb`
|
||
Per evitare riferimenti alla vecchia sidebar: **topnav-v3.js** = aggiunte pagine orfane in nav (Matrice RACI→Struttura interna, Scadenziario→Monitoraggio, Compliance Journey→Guida) + cablato `data-i18n`; **i18n.js** = nuove chiavi nav V3 (gruppi + voci) IT/EN; **AIService::navigationMapBlock** riscritta sul menu in alto V3 (sync con `topnav-v3.js NAV_GROUPS`, non più common-bi.js) + reload php-fpm host+container; **help.js** = corrette 3 "sidebar"; **KB** `nis2_kb` = `docs/kb/V3_UI_NAVIGAZIONE.md` ingerito host-side (`scripts/ingest-v3-ui-doc.php`, 3 chunk SYSTEM, rimossi i chunk V2). Cache-buster `?v=20260627n` sui 3 JS. ⚠️ aria-guide "Mostrami dov'è" → fallback pulito (naviga alla pagina, niente spotlight su mega-menu): rework opzionale.
|
||
|
||
### C. Mail tester
|
||
Annuncio V3 "di nuovo online + cosa provare (menu in alto, Connettori Discovery, ARIA)" a m.tagliavini@/s.fattori@/presidenza@/cristiano.benassati@gmail.com (**4×201** via send-raw). Finestra manutenzione protratta → scuse incluse.
|
||
|
||
### D. Test completo di rilascio (puppeteer) + 2 bug trovati e fixati
|
||
T1–T6 tutti verdi: 36 pagine **200**/no-CDN/no-residui-V2; **tutto il JS** (file + inline di 56 pagine) senza errori di sintassi; **i18n** completo (no chiavi mancanti app); **14 endpoint API** con dati reali coerenti col DB e **nessun leak** multi-tenant; ARIA navigazione allineata al top-nav; pagine pubbliche 200 + manutenzione OFF. **Render autenticato REALE** con **puppeteer-core** (drive `/usr/bin/google-chrome`, `pipe:true` per aggirare il blocco porta-debug dell'env; token QA = mint `jti`+riga `active_sessions`; **chiave org localStorage = `nis2_org_id`**): top-nav si disegna (9 voci, 29 mega-link, FAB, utente/org), dati reali caricano, screenshot ok. 🐞 **Bug 1 (commit `a223923`)**: ARIA data-aware MORTA via chat reale — `AiController::ask` chiamava `requireAuth()` ma non `requireOrgAccess()` → `getCurrentOrgId()` null → `org_data_ok` sempre falso. Fix: risolve l'org da `X-Organization-Id`→param→org primaria, gate membership anti-spoof. 🐞 **Bug 2 (commit `95e77a6`)**: titoli i18n V3 `mr.title`/`normative.title` mancanti → `I18n.t()` ritorna la chiave → l'`<h1>` mostrava "mr.title". Aggiunte le 2 chiavi; cache-buster i18n `?v=20260627p`.
|
||
|
||
### E. Polish 3 note minori — commit `a71d1a0`
|
||
- **aria-guide "Mostrami dov'è" ora funziona su V3**: `data-nav` su voci+mega-link in `topnav-v3.js`; `pointTo` cerca in `.bar-nav` e **apre il mega-menu** (`.aria-guide-open`) puntando la voce; coach "menu in alto". Verificato a video (mega "Rischi" aperto, spotlight su "Inventario"). [supera la nota in B]
|
||
- **segnala-anonimo.html** CSP `font-src 'self'` → `'self' data:` (font `data:` sbloccato).
|
||
- Cache-buster `topnav-v3.js`/`common.js`/`aria-guide.js` `?v=20260627t`.
|
||
- **Ticket NIS2 (ticket-ms, tenant 7): coda VUOTA** — 21 totali tutti RESOLVED/CLOSED, ultimo 25/6, nulla post-mail-V3. Gotcha: `?product=NIS2` ritorna 0 → listare senza filtro + header `x-tenant-id:7`, filtrare client-side su `product`.
|
||
|
||
**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). Tutti pushati. Sessione/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
|