<!-- STANDARD:timezone-conventions:v1.0:start -->
## ⏰ ORARI E TIMEZONE — REGOLE OPERATIVE

> **TL;DR**: l autorità del progetto è **`Europe/Rome`** (CEST estate UTC+2, CET inverno UTC+1).
> Quando scrivi un timestamp, **indica SEMPRE il suffisso TZ** (`CEST`/`CET`/`UTC`) oppure usa ISO8601 con offset (`2026-05-09T16:19:00+02:00`). **Mai** timestamp ambigui.

| Sistema | TZ | Output esempio |
|---|---|---|
| Host Hetzner | `Europe/Rome` | `Sat May 09 16:19 CEST 2026` |
| Container DevEnv (alcuni in drift UTC, vedi standard full) | misto | verifica con `docker exec <c> date` |
| Container produzione | `Europe/Rome` | CEST |
| MySQL `time_zone` | `SYSTEM` | `NOW()` CEST, `UTC_TIMESTAMP()` UTC |
| Apache log `%t` | `Europe/Rome` (locale) | `[09/May/2026:13:04:52 +0200]` |
| Node.js MS | UTC interno | `Date().toISOString()` → `Z` |
| Crontab Hetzner | `Europe/Rome` | `0 3 * * *` = 03:00 italiane |

**Regole**:
1. **Audit/sequenze cross-MS** → UTC obbligatorio (`2026-05-09T14:19:00Z`)
2. **Doc operativi/UI** → CEST/CET con suffisso esplicito
3. **DB store** → UTC, display → locale
4. **Cron critici** → `CRON_TZ=UTC` o fuori finestra DST 02:00-03:00 locale

**DST Italia**: ultima domenica marzo (CET→CEST, ora 02:00 saltata) + ultima domenica ottobre (CEST→CET, ora 02:00-03:00 duplicata).

**Spec completa**: `STANDARD_TIMEZONE_CONVENTIONS.md` (slug `timezone-conventions` v1.0, owner VIGILE).
<!-- STANDARD:timezone-conventions:v1.0:end -->

## 🍎 STANDARD apple-developer-multi-prodotto 2026-05-31 (per chi fa app iOS)

> Standard cross-suite `hub_standards` id=23 v1.0. Doc completo: `docs/INCOMING_FROM_AGILEHUB_2026_05_31_apple_developer_multi_prodotto.md`.

**Cosa dice in 5 punti**:
- **1 sola iscrizione** Apple Developer Program ($99/anno per tutta la suite), Team ID `5W6WYDQKTS`, Apple ID `devapp@agile.software`.
- **Bundle ID convention**: `it.<prodotto>.app` (es. `it.alltax.app`, `it.dfm-pro.app`, `it.nis2.app`).
- **Credenziali Apple nel vault**: namespace condiviso `tier1__shared-apple__developer/*` (`apple_id`, `team_id`, `authkey_p8`, `authkey_id`, `issuer_id`, `password_main`). App vault dedicata `shared-apple-reader` con scope read-only.
- **Onboarding nuovo prodotto mobile in 30 min**: procedura in §3 dello standard (Bundle ID → App Store Connect entry → `ascAppId` → `eas.json` → `eas credentials` → primo build). Da quel momento il prodotto builda + submitta autonomo.
- ⚠️ **NON committare MAI** `.p8` o password Apple. Aggiungere al `.gitignore`: `*.p8`, `AuthKey_*.p8`, `apple-developer.txt`, `appstore-connect-api-key.txt`.

**Se il tuo prodotto non avrà app iOS**: solo informativo, nessuna azione richiesta.

**Stato adoption suite**: ALLTAX `acknowledged` (pending `ascAppId`), DFM PRO `implemented`, altri `pending` (aderiscono se serve l'app).

---


## 🔵 VAULT-STEWARD chain fix 2026-05-31 (informativo)

> Aggiunto da VIGILE. Doc completo: `docs/INCOMING_FROM_AGILEHUB_2026_05_31_vault_chain_fix.md`.

**Cosa è cambiato nel vault-steward** (impatta solo chi fa troubleshooting/audit del vault):

- `vault-cli.js audit [--last=N]` → ✅ **ora funziona** (era rotto con `Incorrect arguments to mysqld_stmt_execute`, bug LIMIT prepared param di mysql2).
- `vault-cli.js verify-chain` → ⚠️ mostra ancora `❌ Chain BROKEN at id=257` — è **storico** (1645 race break documentati dal 25/4 al 31/5 per race condition cronica in `audit.log()`), **NON tampering, NON attacco**. Fix applicato (transazione + `SELECT FOR UPDATE` per serializzare writer concorrenti). Da `id=23793` in poi la chain HMAC è di nuovo affidabile (entry marker `audit_chain_reanchor`).
- Le tue letture credenziali via wrapper vault al boot funzionano **identiche a prima** — zero cambiamenti lato API.

**Stress test post-fix**: 50 letture concorrenti → 0 nuovi race break (1647 → 1647 invariato).

**File modificati host-side**: `/opt/vault-steward/{audit.js, db.js, vault-cli.js}`, container ricreato con 2 nuovi bind-mount (downtime ~5s, nessun MS in regressione). Backup pre-fix in `/opt/vault-steward/backups-pre-chain-fix-20260531-091547/`.

---


## 🔴 AGGIORNAMENTI AGILEHUB 2026-05-31 (vincolanti)

> Aggiunto da AgileHub-side (VIGILE) su direttiva utente: "se devi segnalare aggiornamenti devi farlo per tutti i dockers dev". Doc completo: `docs/INCOMING_FROM_AGILEHUB_2026_05_31_email_send_fix_and_php_opcache.md`.

### 1. email-automation-ms — bug "invii silenti" CHIUSO (per TUTTI i prodotti che mandano email)

Bug: `POST /api/emails/send` con payload `{to, subject, html: "..."}` (campo `html` invece del canonico `body`) rispondeva `201 success:true` ma spediva email **vuota** (solo subject). Fixed (agile-services commit `809ea53`): defense-in-depth in `emailService.send()` + alias `html` su `/emails/send` + 400 EMPTY_RENDERED_BODY.

**Payload canonici da subito**:
- Template Handlebars: `POST /emails/send {to, template, data, product, tenantId}`
- HTML grezzo: `POST /emails/send-raw {to, subject, html, product, tenantId}` ← preferito per HTML diretto

**Da controllare lato vostro**: caller che usano `html` su `/emails/send` (oggi alias-compat con header `Deprecation`) → migrare a `/emails/send-raw`; gestire `400 EMPTY_RENDERED_BODY` nel codice client; assicurarsi di usare `data` (non `variables`) per i template.

### 2. PHP opcache USR2 + disciplina commit (solo prodotti PHP-FPM)

**Hot-reload obbligatorio**: dopo OGNI edit `.php` → `docker exec <container-fpm-prodotto> kill -USR2 1`. Bind-mount NON basta (`opcache.validate_timestamps=Off` → vecchio bytecode in cache, gli utenti vedono ancora la versione precedente). USR2 = graceful FPM reload, zero downtime, ~10ms.

**Commit-early**: appena una modifica funziona (smoke verde post-USR2), commit subito. Il cron `ticket-agent-cron.sh` può revertare WIP scoperte (caso reale NIS2 29/5: commit `d5d83bb` revertato `index.php` di una Feature 1 non committata). Per WIP attivo prolungato → semaforo manuale:
```bash
echo "USER=... STARTED=$(date -Iseconds)" > /tmp/agent-working.lock
# il cron salta i container con quel lock
rm /tmp/agent-working.lock
```

**Push se cache token vuota** (post-reboot container legacy, oggi solo `trpg-agile` migrato al vault helper): chiedere a VIGILE/AgileHub-side "pusha N commit dall'host" (helper vault). Fix definitivo = migrazione vault helper come `trpg-agile`.

---


## 🔴 ATTIVITÀ PRIMARIA — Workflow di rilascio + hot-reload PHP + disciplina commit

> **VINCOLANTE per OGNI modifica di codice su NIS2.** Aggiunto 2026-05-30 da AgileHub-side (VIGILE) dopo che la sessione NIS2 ha scoperto sul campo che il bind-mount NON serve codice "live" e che le modifiche scoperte vengono revertate.

**Le 3 regole d'oro** (doc completo in `docs/INCOMING_FROM_AGILEHUB_2026_05_30_release_workflow_hot_reload.md`):

### 1. Hot-reload PHP — dopo OGNI edit `.php`

```bash
docker exec <container-fpm-nis2> kill -USR2 1
```

Il bind-mount NON basta: `opcache.validate_timestamps=Off` → senza USR2 gli utenti vedono ancora il vecchio bytecode. USR2 = graceful FPM reload (zero downtime, ~10ms, request in volo finiscono).

⚠️ **La sezione "ARCHITETTURA: PHP-FPM con BIND MOUNT (LIVE)" più sotto è fuorviante**: il filesystem è live via bind-mount, ma il **bytecode servito** no, finché non fai `kill -USR2 1`. Aggiornare quella sezione a riflettere la realtà.

### 2. Commit immediato — niente modifiche scoperte

Appena una modifica funziona (smoke verde post-USR2):
```bash
git add <file> && git commit -m "[FEAT|FIX|DOCS] descrizione"
```

**Mai** lasciare modifiche scoperte nel working tree: il cron `ticket-agent-cron.sh` (ogni 2 min) può lanciare `claude -p` che rebase/reverta. **Caso reale 29/5**: commit `d5d83bb` (agent automatico) ha revertato `index.php` di una Feature 1 non committata → persa.

Per WIP attivo prolungato → semaforo manuale (il cron salta i container con quel lock):
```bash
echo "USER=cristiano STARTED=$(date -Iseconds)" > /tmp/agent-working.lock
# ... lavori ...
rm /tmp/agent-working.lock
```

### 3. Push via host se cache token vuota (post-reboot container)

Il container NIS2 è ancora legacy (non migrato all'helper credenziali vault come `trpg-agile`). Dopo un reboot del container la `git credential-cache` in-memory è vuota → push bloccato. Invece di `git-login` interattivo, **chiedi a VIGILE/AgileHub-side**:
> "VIGILE, NIS2 ha N commit su main da pushare, fallo tu dall'host"

L'host è migrato all'helper vault, prende il PAT Gitea automatico, pusha sul bind-mount condiviso, ripristina ownership `.git`. Fix definitivo = migrazione vault helper anche per nis2-agile (richiede recreate container).

### Workflow operativo per ogni modifica

```
edit → kill -USR2 1 → smoke (curl) → bump app/version.json → git commit → git push (via host se serve)
```

### Cosa NON serve (semplificazione rispetto a TRPG)

NIS2 è **L1 master-shared** (1 istanza, `nis2.agile.software`). NON servono: plan TSSP in `hub_upgrade_plans`, image Docker build, agent run, retag registry. Quelli sono pattern TRPG **L2 partial-SaaS** per propagare a N tenant — NIS2 ha 1 sola istanza, l'edit + USR2 **È** la propagazione. Tabella di confronto nel doc completo.

📄 **Doc completo**: `docs/INCOMING_FROM_AGILEHUB_2026_05_30_release_workflow_hot_reload.md`

---


# NIS2 Agile - Documentazione Progetto

## REGOLE DI GOVERNANCE (LEGGERE ATTENTAMENTE, aggiornate 2026-04-22)

> **Queste regole sono OBBLIGATORIE e non negoziabili.**

### REGOLA FONDAMENTALE: Gitea = SOLO Backup

> **Gitea e un BACKUP one-way (sorgente -> Gitea), NON la fonte di verita.**
> **Il webhook auto-pull e DISABILITATO su tutti i 13 repo dal 2026-04-22.**
>
> - Le modifiche che fai nel container sono GIA live su `/var/www/nis2-agile/` via bind mount
> - `/var/www/nis2-agile/` e la FONTE DI VERITA
> - NON proporre MAI "git pull da Gitea" per applicare modifiche
> - Per tirare giu qualcosa da Gitea serve richiesta esplicita dell utente
> - git push -> Gitea = OK (backup)
> - git pull da Gitea -> `/var/www/nis2-agile/` = NO (puo sovrascrivere modifiche vere)

### ARCHITETTURA: PHP-FPM in Docker con BIND MOUNT (LIVE)

> **Verificato 2026-04-22**: NIS2 gira con:
> - `nis2-app` (php-fpm) con bind mount **RW** su `/var/www/nis2-agile/application` e `/public`
> - `nis2-web` (nginx) con bind mount **RO** su `/public`
> - `nis2-db` (MySQL) per persistenza
> - `nis2-qdrant` (vector DB)
> - Apache esterno ha `DocumentRoot /var/www/nis2-agile/public`

**Cosa va LIVE ISTANTANEAMENTE:**
- File `.php` in `/var/www/nis2-agile/application/` -- PHP-FPM rilegge ad ogni request
- File in `/var/www/nis2-agile/public/` -- serviti da nginx (via bind mount :ro)
- File `.html/.css/.js` nel public -- live via nginx/Apache

**Cosa richiede azione (CHIEDI SEMPRE CONFERMA):**
- Modifiche a `docker/nginx.conf` -> `docker restart nis2-web`
- Modifiche al Dockerfile -> `docker compose build + up -d`
- Schema DB (`nis2-db`) -> SQL manuale
- Worker php cron/feedback -> attendono il prossimo run o restart

**Nota**: non serve MAI fare rebuild di nis2-app per cambi di codice PHP -- il bind mount :rw garantisce che php-fpm legga sempre la versione aggiornata.


### Cosa PUOI fare autonomamente:
- Leggere codice sorgente e documentazione
- Eseguire query SELECT sul database
- Analizzare log (Apache, Docker, PM2)
- Proporre modifiche e mostrare diff (SENZA applicarle)
- Verificare stato dei servizi

### Cosa richiede CONFERMA dell utente:
- **Modificare QUALSIASI file** (potrebbe essere live istantaneamente!)
- **git commit e git push** (e un backup, ma sempre da confermare)
- **Modifiche schema DB** (ALTER/CREATE/DROP TABLE)
- **INSERT/UPDATE/DELETE** su dati di produzione
- **Installazione dipendenze** (composer require, npm install)
- **Modifiche a configurazione** (.env, docker-compose.yml, vhost Apache)
- **docker compose build/restart**, **pm2 restart**, **systemctl** qualsiasi

### DIVIETI ASSOLUTI:
- **MAI fare git pull** da Gitea senza richiesta esplicita
- **MAI fare git reset --hard** o operazioni distruttive
- **MAI toccare altri progetti o container**
- **MAI modificare configurazioni di sistema** (Apache globale, PHP globale, MySQL root)
- **MAI cancellare dati** senza backup e conferma utente
- **MAI tentare deploy SSH/SCP** verso altri server

### Flusso CORRETTO per una modifica:
1. **Analizza**: leggi il codice, capisci il problema
2. **Proponi**: mostra le modifiche all utente (diff) SENZA applicarle
3. **Attendi conferma**: l utente decide se procedere
4. **Applica**: solo dopo conferma
5. **Distingui**: e live subito o serve un rebuild/restart? Di all utente chiaramente
6. **Verifica**: controlla che https://nis2.agile.software funzioni (se applicabile)
7. **Backup su Gitea**: git commit + push (solo dopo conferma utente)

### Se qualcosa va storto:
- **NON tentare fix distruttivi** (reset, force push, drop, rm -rf)
- **NON proporre `git pull`** come recupero
- Comunica il problema all utente con dettagli precisi


## PRIMA DI INIZIARE
- Leggi sempre questo file prima di iniziare qualsiasi lavoro
- Il progetto e' al **100% di completamento + Sprint Simulazioni + Audit Chain + Sistema Feedback AI** (~34.000 righe, 85+ file sorgente)
- 15 commit su main, tutto deployato e testato su Hetzner
- E2E test completati, bug fixing, Docker verificato, UI polished
> 3. `docs/CONTEXT_LAST_SESSION.md` - **Contesto ultima sessione (continuita cross-browser)**
>
> Poi dimmi cosa hai capito dello stato attuale e dove eravamo rimasti.

## A FINE SESSIONE

> **OBBLIGATORIO**: Prima di chiudere, aggiornare SEMPRE:
>
> Aggiorna `docs/CONTEXT_LAST_SESSION.md` con:
> - Data sessione
> - Cosa hai fatto in questa sessione
> - File creati o modificati
> - File deployati su Hetzner
> - Problemi aperti / errori non risolti
> - Prossimi passi consigliati
>
> Se hai modificato schema DB, architettura o URL, aggiorna anche CLAUDE.md.


## Panoramica
NIS2 Agile e' una piattaforma SaaS multi-tenant per supportare le aziende nella compliance alla Direttiva NIS2 (EU 2022/2555) e al D.Lgs. 138/2024 italiano. Include AI integration (Claude API) per gap analysis, generazione policy, classificazione incidenti e suggerimenti rischi.

Target: PMI, Enterprise, Consulenti/CISO.

## Stack Tecnologico
- Backend: PHP 8.4 vanilla (no framework, Front Controller pattern)
- Database: MySQL 8.x (nis2_agile_db)
- Frontend: HTML5/CSS3/JavaScript vanilla
- Auth: JWT HS256 (2h access + 7d refresh)
- AI: Anthropic Claude API (claude-sonnet-4-5-20250929)
- Server: Hetzner CPX31 (135.181.149.254)
- VCS: Gitea (git.certisource.it)
- URL Produzione: https://nis2.agile.software/


## Visibilita Cross-Project

### agile-services (Read/Write BIDIREZIONALE)

**IMPORTANTE**: Questo progetto utilizza i microservizi condivisi di **agile-services** con accesso **Read/Write bidirezionale**.

#### Regole di integrazione
1. **Leggere SEMPRE** `agile-services-istructio.md` nella root del progetto per capire come interagire con i servizi
2. **Puoi leggere E modificare** i file in agile-services (path locale: `c:\Projects\agile-services`, path container: `/projects/agile-services`)
3. I servizi disponibili includono: vault, document, payment, certificate, subscription, billing, practice, company, supplier, investigation, whatsapp
4. Le credenziali di accesso ai servizi sono documentate in `agile-services-istructio.md`
5. Ogni nuovo servizio creato per NIS2 deve essere registrato anche in agile-services per essere riutilizzabile
6. **agile-services puo' leggere e modificare file di NIS2** — la visibilita e' bidirezionale

#### Accesso
- Path locale: `c:\Projects\agile-services`
- Path container: `/projects/agile-services` (read-write)
- File istruzioni: `./agile-services-istructio.md` (nella root del progetto)

### trpg.agile (Read-Only)

NIS2 puo' **leggere** il codice di TRPG Agile per riferimento e pattern, ma **NON deve modificare** nessun file.

- Path locale: `c:\Projects\trpg.agile`
- **Permessi**: SOLO LETTURA
- **Scopo**: Consultare pattern UI/UX, architettura frontend, convenzioni codice
- **DIVIETO**: NON modificare, NON creare, NON cancellare file in trpg.agile

### lg231.agile (Read-Only)

NIS2 puo' **leggere** il codice di 231 Agile per riferimento e pattern, ma **NON deve modificare** nessun file.

- Path locale: `c:\Projects\lg231.agile`
- **Permessi**: SOLO LETTURA
- **Scopo**: Consultare architettura microservizi PHP/Slim 4, pattern multi-tenancy, struttura database
- **DIVIETO**: NON modificare, NON creare, NON cancellare file in lg231.agile

### sustainai.agile (Read-Only)

NIS2 puo' **leggere** il codice di SustainAI Agile per riferimento e pattern, ma **NON deve modificare** nessun file.

- Path locale: `c:\Projects\sustainai.agile`
- **Permessi**: SOLO LETTURA
- **Scopo**: Consultare pattern UI/UX (identico a NIS2), struttura frontend, convenzioni CSS/JS
- **DIVIETO**: NON modificare, NON creare, NON cancellare file in sustainai.agile

### Riepilogo Visibilita

| Progetto | Permesso | Direzione |
|----------|----------|-----------|
| **agile-services** | Read/Write | Bidirezionale (NIS2 <-> agile-services) |
| **trpg.agile** | Read-Only | NIS2 -> trpg (solo lettura) |
| **lg231.agile** | Read-Only | NIS2 -> lg231 (solo lettura) |
| **sustainai.agile** | Read-Only | NIS2 -> sustainai (solo lettura) |

> **REGOLA**: Non accedere MAI a progetti non elencati in questa tabella. Nel dubbio, chiedere all'utente.

## Regola Fondamentale
Il progetto NIS2 Agile e' COMPLETAMENTE ISOLATO dagli altri applicativi (CertiSource, AGILE_DFM). Database dedicato, utente dedicato, path dedicati. Non condividere MAI credenziali tra applicativi.

## Struttura Progetto
```
nis2.agile/
├── CLAUDE.md                          # Questo file
├── .env                               # Variabili ambiente (NON committare)
├── .gitignore
├── application/
│   ├── config/
│   │   ├── config.php                 # Costanti app, CORS, JWT, AI, rate limiting
│   │   ├── database.php               # Classe Database (PDO singleton)
│   │   └── env.php                    # Caricamento .env
│   ├── controllers/                   # 19 controller (tutti implementati 100%)
│   │   ├── BaseController.php         # Auth JWT, multi-tenancy, JSON responses (576 righe)
│   │   ├── AdminController.php        # Gestione piattaforma (super_admin)
│   │   ├── AssessmentController.php   # Gap analysis e questionari NIS2 (80 domande)
│   │   ├── AssetController.php        # Inventario asset e dipendenze
│   │   ├── AuditController.php        # Controlli, evidenze, report, export CSV
│   │   ├── AuthController.php         # Login, register, JWT, rate limiting
│   │   ├── DashboardController.php    # Overview, score, deadlines, heatmap
│   │   ├── IncidentController.php     # Incidenti Art.23 (24h/72h/30d) + email
│   │   ├── NonConformityController.php# NCR/CAPA non-conformità e azioni correttive
│   │   ├── OnboardingController.php   # Wizard onboarding con visura/CertiSource
│   │   ├── OrganizationController.php # CRUD org, membri, classificazione NIS2
│   │   ├── PolicyController.php       # Policy, approvazione, AI generation
│   │   ├── RiskController.php         # Risk register, trattamenti, matrice, AI suggest
│   │   ├── SupplyChainController.php  # Fornitori, valutazione, risk overview
│   │   ├── TrainingController.php     # Corsi, assegnazioni, compliance formativa
│   │   ├── ServicesController.php     # Services API (read-only, API Key + scope)
│   │   ├── WebhookController.php      # CRUD api_keys + webhook_subscriptions
│   │   ├── WhistleblowingController.php # Segnalazioni anonime Art.32 NIS2
│   │   ├── NormativeController.php    # Feed NIS2/ACN/DORA con ACK tracciato
│   │   └── FeedbackController.php     # Sistema segnalazioni bug/UX con risoluzione AI autonoma
│   ├── services/                      # 6 servizi
│   │   ├── AIService.php              # Anthropic Claude API (gap, risk, policy, incident)
│   │   ├── EmailService.php           # Email CSIRT, training, welcome, invite
│   │   ├── RateLimitService.php       # Rate limiting file-based
│   │   ├── ReportService.php          # Report esecutivo HTML, export CSV
│   │   ├── WebhookService.php         # Delivery webhook HMAC-SHA256, retry 3x
│   │   └── FeedbackService.php        # createReport, classifyWithAI, broadcastResolution
│   │   └── VisuraService.php          # AI extraction PDF visura + CertiSource API
│   ├── models/                        # (vuoto - logica nei controller)
│   └── data/
│       └── nis2_questionnaire.json    # 80 domande gap analysis (10 categorie Art.21)
├── public/
│   ├── index.php                      # Front Controller / Router
│   ├── .htaccess                      # Rewrite rules + Authorization header
│   ├── api-status.php                 # Health check
│   ├── index.html                     # Landing page
│   ├── login.html                     # Login
│   ├── register.html                  # Registrazione
│   ├── onboarding.html                # Wizard 5-step (visura/CertiSource/manuale)
│   ├── setup-org.html                 # Setup org (legacy, ora usa onboarding.html)
│   ├── dashboard.html                 # Dashboard principale
│   ├── assessment.html                # Gap analysis wizard
│   ├── risks.html                     # Risk management + matrice 5x5
│   ├── incidents.html                 # Gestione incidenti Art.23
│   ├── policies.html                  # Policy management + AI generate
│   ├── supply-chain.html              # Fornitori + assessment sicurezza
│   ├── training.html                  # Formazione + assegnazioni
│   ├── assets.html                    # Inventario asset
│   ├── reports.html                   # Report compliance + audit log
│   ├── settings.html                  # Impostazioni org/profilo/membri
│   ├── companies.html                 # Gestione aziende (consulente)
│   ├── architecture.html              # Pagina architettura sistema
│   ├── admin/
│   │   ├── index.html                 # Admin dashboard
│   │   ├── organizations.html         # Gestione organizzazioni
│   │   └── users.html                 # Gestione utenti
│   ├── css/
│   │   └── style.css                  # CSS principale (~1600 righe)
│   ├── js/
│   │   ├── api.js                     # Client API (270 righe, tutti gli endpoint)
│   │   ├── common.js                  # Utility condivise (sidebar, notifiche, etc.)
│   │   ├── i18n.js                    # Internazionalizzazione IT/EN
│   │   └── help.js                    # Help contestuale online
│   └── uploads/                       # Upload directory (gitignored)
│       └── visure/                    # PDF visure camerali
├── docker/
│   ├── Dockerfile
│   ├── docker-compose.yml
│   ├── nginx.conf
│   └── php.ini
└── docs/
    ├── sql/
    │   ├── 001_initial_schema.sql     # Schema DB completo (20 tabelle)
    │   ├── 002_email_log.sql          # Tabella email_log
    │   ├── 003_voluntary_compliance.sql # ALTER organizations: voluntary_compliance
    │   ├── 004_ncr_capa.sql           # Tabelle non_conformities, corrective_actions
    │   ├── 005_consultant_support.sql # ALTER user_organizations: ruolo consultant
    │   ├── 006_security_improvements.sql # Indici performance + soft delete + trigger audit immutabile
    │   ├── 007_services_api.sql         # api_keys, webhook_subscriptions, webhook_deliveries
    │   ├── 008_whistleblowing.sql       # whistleblowing_reports, whistleblowing_timeline
    │   ├── 009_normative_updates.sql    # normative_updates, normative_ack (seed 5 aggiornamenti)
    │   ├── 010_audit_hash_chain.sql     # prev_hash, entry_hash, severity su audit_logs
    │   └── reset-demo.sql              # Reset dati demo (mantiene id<=4)
    ├── context/
    │   └── CONTEXT_SCHEMA_DB.md
    ├── prompts/
    └── credentials/
        ├── credentials.md
        └── hetzner_key                # SSH key per Hetzner
```

## Multi-Tenancy
- Ogni tabella dati ha `organization_id`
- Header `X-Organization-Id` per selezionare org attiva
- Ruoli: `super_admin`, `org_admin`, `compliance_manager`, `board_member`, `auditor`, `employee`, `consultant`
- `requireOrgAccess()` in BaseController verifica membership
- Super admin bypassa tutti i controlli di membership

## Flusso Utente
1. **Registrazione** → redirect a `onboarding.html`
2. **Onboarding** (5 step): Scelta metodo → Visura/CertiSource/Manuale → Dati aziendali → Profilo → Classificazione NIS2
3. **Login** → se ha org → `dashboard.html`, altrimenti → `onboarding.html`
4. **Dashboard** → navigazione sidebar a tutti i moduli

## Database (29 tabelle)
organizations, users, user_organizations, refresh_tokens, assessments, assessment_responses, risks, risk_treatments, incidents, incident_timeline, policies, suppliers, training_courses, training_assignments, assets, compliance_controls, evidence_files, audit_logs, ai_interactions, email_log, non_conformities, corrective_actions

Schema: `docs/sql/` (9 migrazioni: 001→009)

## Servizi

### AIService.php
- `analyzeGapAssessment()` - Analisi assessment con raccomandazioni
- `suggestRisks()` - Suggerimenti rischi per settore/asset
- `generatePolicy()` - Generazione bozze policy NIS2
- `classifyIncident()` - Classificazione e severity incidenti
- Modello: claude-sonnet-4-5-20250929, API: https://api.anthropic.com/v1/messages

### EmailService.php
- Notifiche CSIRT: early warning 24h, notification 72h, final report 30d
- Training assignment e reminder
- Welcome email, member invite
- Template HTML professionale con branding NIS2 Agile

### RateLimitService.php
- File-based (/tmp/nis2_ratelimit/)
- Login: 5/min, 20/h | Register: 3/10min | AI: 10/min, 100/h

### ReportService.php
- Report esecutivo HTML (stampabile come PDF)
- Export CSV: rischi, incidenti, controlli, asset (separatore ;, BOM UTF-8)

### VisuraService.php
- Estrazione AI da PDF visura camerale (Claude con document type)
- Fetch dati da CertiSource API (GET /api/company/enrich?vat=)
- Mapping ATECO → settore NIS2

### AuditService.php (NUOVO)
- Hash chain SHA-256: ogni record include prev_hash → entry_hash linkato
- `log()`: inserisce con hash catena, auto-severity (info/warning/critical)
- `verifyChain()`: verifica integrità per org, rileva record manomessi
- `exportCertified()`: export JSON con SHA-256 del contenuto, salva in audit_exports
- Adattato da lg231-agile/shared/audit-lib/src/AuditTrailService.php

### WebhookService.php (NUOVO)
- Delivery HMAC-SHA256 Stripe-like, header X-NIS2-Signature
- Retry 3x backoff (0s/5min/30min), log in webhook_deliveries

### FeedbackService.php (NUOVO)
- `createReport()`: INSERT + classifyWithAI() sincrono (10s timeout)
- `classifyWithAI()`: chiama AIService::classifyFeedback(), aggiorna DB silenziosamente
- `broadcastResolution()`: email a tutti i membri org via EmailService::sendFeedbackResolved()
- Worker autonomo: `scripts/feedback-worker.php` (cron 30min, docker exec nis2-agile-devenv + Claude Code CLI)

### simulate-nis2.php (ROOT — NUOVO)
- Script simulazione demo 3 aziende × 5 scenari (CLI + SSE streaming)
- Aziende: DataCore (IT/Essential), MedClinic (Sanità/Important), EnerNet (Energia/Critical)
- Scenari: Onboarding + Assessment | Ransomware Art.23 | Data Breach | Whistleblowing | Audit Chain
- Reset: docs/sql/reset-demo.sql (cancella org/utenti con id>4 e email %.demo%)

## Deploy
- **SSH**: `ssh -i docs/credentials/hetzner_key root@135.181.149.254`
- **Path server**: `/var/www/nis2-agile/`
- **Apache config**: `/etc/apache2/sites-enabled/nis2-agile-software.conf` (HTTP) + `nis2-agile-software-le-ssl.conf` (HTTPS, dopo DNS+certbot)
- **Deploy**: `cd /var/www/nis2-agile && git pull origin main`
- **DB**: Vedi `docs/DB_ACCESS.md` per credenziali (password in `.env`)
- **Attivazione SSL nis2.agile.software**:
  1. Cloudflare: aggiungere `nis2.agile.software A 135.181.149.254` (proxy OFF — grigio)
  2. Hetzner: `bash /opt/devenv/scripts/setup-nis2-agile-software.sh`
- **Vecchio dominio**: `nis2.certisource.it` resterà attivo finché redirect non è configurato dallo script

## Git
- **Repository**: https://git.certisource.it/AdminGit2026/nis2-agile
- **Token Gitea**: Configurato in git credential manager (non documentare qui)
- **Branch**: main
- **Commit format**: `[AREA] Descrizione`

### Cronologia Commit
```
7080695 [FEAT] Ruolo Consulente + Wizard Registrazione v2
ba21534 [DEPLOY] Migrazione a subdomain nis2.agile.software
92f9366 Merge branch 'main'
d3eac7c [CORE] Rimosso credenziali da CLAUDE.md + aggiunto docs/DB_ACCESS.md
a0fd543 [CORE] Aggiunto settings Claude Code con permessi ampi
0a73983 [FIX] Dockerignore: allow docker/php.ini for build context
4bd2326 [CORE] Aggiunto integrazione agile-services
52fd45f [FEAT] i18n IT/EN, Help Online contestuale, pagina Architettura
4e3408e [FEAT] Visura auto-fill, adesione volontaria, modulo NCR/CAPA
517cab7 [FIX] Fix annual_turnover field name in setup-org.html
68f8cab [POLISH] Docker setup fix + UI polish + project completion
bcc5a2b [FIX] E2E testing - fix router, EmailService, frontend data mapping
```

## API Endpoints Completi

Base: `/api/{controller}/{action}/{id?}` (su subdomain https://nis2.agile.software/)

### Auth: POST register, login, logout, refresh, change-password | GET me | PUT profile
### Organizations: POST create, classify | GET current, list, {id}/members | PUT {id} | POST {id}/invite | DELETE {id}/members/{sid}
### Assessments: GET list, {id}, {id}/questions, {id}/report | POST create, {id}/respond, {id}/complete, {id}/ai-analyze | PUT {id}
### Dashboard: GET overview, compliance-score, upcoming-deadlines, recent-activity, risk-heatmap
### Risks: GET list, {id}, matrix | POST create, {id}/treatments, ai-suggest | PUT {id}, treatments/{sid} | DELETE {id}
### Incidents: GET list, {id} | POST create, {id}/timeline, {id}/early-warning, {id}/notification, {id}/final-report, {id}/ai-classify | PUT {id}
### Policies: GET list, {id}, templates | POST create, {id}/approve, ai-generate | PUT {id} | DELETE {id}
### Supply Chain: GET list, {id}, risk-overview | POST create, {id}/assess | PUT {id} | DELETE {id}
### Training: GET courses, assignments, compliance-status | POST courses, assign | PUT assignments/{sid}
### Assets: GET list, {id}, dependency-map | POST create | PUT {id} | DELETE {id}
### Audit: GET controls, evidence/list, report, logs, iso27001-mapping, executive-report, export | PUT controls/{sid} | POST evidence/upload
### Onboarding: POST upload-visura, fetch-company, complete
### Admin: GET organizations, users, stats
### NCR/CAPA: GET list, {id}, stats | POST create, fromAssessment, {id}/capa, {id}/sync, webhook | PUT {id}, capa/{subId}
### Services API (X-API-Key): GET status, compliance-summary, risks-feed, incidents-feed, controls-status, assets-critical, suppliers-risk, policies-approved, openapi
### Webhooks: GET api-keys, subscriptions, deliveries | POST api-keys, subscriptions, subscriptions/{id}/test, retry | PUT subscriptions/{id} | DELETE api-keys/{id}, subscriptions/{id}
### Whistleblowing: POST submit, {id}/assign, {id}/close | GET list, {id}, stats, track-anonymous | PUT {id}
### Normative: GET list, {id}, pending, stats | POST {id}/ack, create
### Feedback: POST submit | GET mine, list, {id} | PUT {id} | POST {id}/resolve

## Stato Completamento
Tutti i moduli sono implementati e testati:
- [x] Test end-to-end: tutti gli endpoint API verificati
- [x] Bug fixing: router rewrite, EmailService parse fix, frontend data mapping
- [x] Docker setup: Dockerfile, docker-compose.yml, nginx.conf, php.ini verificati
- [x] UI polish: animazioni, skeleton loaders, mobile backdrop, print styles, a11y

### Bug Risolti (E2E Testing)
1. **Router /{id}/subAction** - Pattern matching riscritto completamente per gestire GET /assessments/1/questions, GET /organizations/2/members, etc.
2. **EmailService parse error** - PHP non supporta `??` dentro `{$var}` string interpolation, estratto a variabile
3. **Frontend data mapping** - Dashboard, Assessment, Onboarding avevano nomi campo diversi dal backend
4. **Field name mismatches** - annual_turnover→annual_turnover_eur, question_id→question_code, compliance_level→response_value

*Ultimo aggiornamento: 2026-02-20*

## Infrastruttura DevEnv

| Risorsa | Valore |
|---------|--------|
| **Container** | nis2-agile-devenv |
| **IDE** | https://certisource.it/dev-nis2-ide/ |
| **API** | https://certisource.it/dev-nis2-api/ |
| **Browser** | https://certisource.it/dev-nis2-browser/ |
| **Porte** | 8454 / 3046 / 3047 / 6091 |
| **Password IDE** | Nis2AgileDev2026! |
| **Produzione** | https://nis2.agile.software/ |

## Documentazione Commerciale AgentAI (aggiornato 2026-03-09)

> QUESTO ANNULLA LE ISTRUZIONI PRECEDENTI sulla documentazione commerciale.
> Standard completo: /opt/devenv/COMMERCIAL_STANDARDS.md
> Stato prodotti: /opt/agent-ai/hub/AGENTAI_SPECS.md

### Regole NUOVE (in vigore)
- Landing page e presentazione vanno nel TUO repo (docs/agentai/ o dove preferisci)
- products.json (/opt/agent-ai/hub/products.json) e SOLO un indice con URL assoluti
- Aggiorna products.json con https://tuo-dominio/path-al-file.html
- La landing DEVE includere un link/bottone per registrazione o richiesta informazioni
- mktg.agile.software legge products.json per campagne marketing

### Regole OBSOLETE (non fare piu)
- NON scrivere file in /opt/agent-ai/hub/landing/
- NON scrivere file in /opt/agent-ai/hub/presentations/
- NON usare path relativi in products.json
- Le directory landing/ e presentations/ nel hub sono VUOTE

### Workflow
1. Scrivi landing/presentazione nel tuo repo
2. Commit + push (backup su Gitea (webhook DISABILITATO dal 2026-04-22))
3. Chiedi conferma utente
4. Aggiorna products.json con URL assoluto
5. Verifica URL raggiungibile

### REGOLA
> SEMPRE chiedere conferma utente PRIMA di generare documenti commerciali.
## REGOLA: Sincronizzazione CLAUDE.md
- Dopo QUALSIASI modifica a: URL produzione, dominio, porta, path, schema DB, architettura -> **AGGIORNARE CLAUDE.md IMMEDIATAMENTE**
- CLAUDE.md e la "single source of truth" del progetto
- A fine sessione: verificare che CLAUDE.md rifletta lo stato reale

---

## Knowledge Base Multi-Livello (Migration 012-014 - 2026-04-11)

### Cosa e cambiato
NIS2 ora ha un sistema RAG completo con visibilita' a 3 livelli (SYSTEM/FIRM/ORG), coerente col pattern gia' applicato a TRPG e SustainAI. L'AI puo' rispondere alle domande pescando da documenti caricati dai consulenti o dai responsabili compliance.

| Scope | Chi possiede | Chi vede |
|---|---|---|
| SYSTEM | Vendor (Agile Tech) | Tutti gli utenti del prodotto |
| FIRM | Studio di consulenza (consulting_firm_id) | Tutti i collaboratori dello studio + organizations esplicitamente condivise |
| ORG | Singola organization cliente | Solo gli utenti di quella org (org_admin/compliance_manager) |

### Stack RAG nuovo
- **nis2-qdrant** (container nuovo): qdrant/qdrant:v1.7.4, network nis2-net, IP fisso 172.21.0.5 (workaround DNS musl Alpine - vedi sotto).
- Voyage AI embeddings (`voyage-3-lite`, 512 dim, output_dimension=512). Chiave shared con sustainai.
- Collection Qdrant: `nis2_kb` (Cosine, 512 dim).

### Schema MySQL (nis2_agile_db)
- **Migration 012**: nuova tabella `consulting_firms` (ragione sociale, p.iva, plan, max_organizations, max_users, status). ALTER `users.consulting_firm_id` e `organizations.consulting_firm_id`.
- **Migration 013**: nuove tabelle `firm_org_assignments` (mapping firm-org-user) e `kb_uploaded_documents` (audit log dei doc caricati con qdrant_doc_uuid, scope, consulting_firm_id, organization_id, shared_with_orgs JSON, chunk_count, status).

### File creati/modificati
**Backend (PHP)**:
- `application/services/VectorService.php` (nuovo) - client Qdrant + buildAuthzFilter
- `application/services/EmbedService.php` (nuovo) - client Voyage AI
- `application/services/RagService.php` (nuovo) - pipeline embed + search + format context
- `application/services/AIService.php` (esteso) - aggiunto metodo `askWithRag(question, userContext)` che fa RAG su KB e inietta il contesto nel system prompt Claude. Fallback graceful se RAG non disponibile.
- `application/controllers/KnowledgeBaseController.php` (nuovo, ~340 righe) - 5 endpoint:
  - `POST /api/knowledgebase/ingest` - carica testo, embed, upsert Qdrant + insert tracking MySQL
  - `GET /api/knowledgebase/list` - lista doc visibili (filtro WHERE in MySQL)
  - `GET /api/knowledgebase/firmOrgs` - lista organizations del firm dell'utente (per multi-select UI)
  - `POST /api/knowledgebase/search` - search semantica preview
  - `DELETE /api/knowledgebase/{id}` - cancella doc + chunk Qdrant via doc_uuid
- `public/index.php` (esteso) - registrato `knowledgebase` nel controllerMap + actionMap

**Schema SQL**:
- `docs/sql/012_consulting_firms.sql` (nuovo)
- `docs/sql/013_firm_assignments.sql` (nuovo)

**Frontend**:
- `public/kb.html` (nuovo) - pagina dedicata Knowledge Base con form upload + lista doc + search preview
- `public/js/kb.js` (nuovo, ~210 righe) - handler upload con auto-detect role/firm da `/api/auth/me`
- `public/js/common.js` (esteso) - voce "Knowledge Base" (icona libro) aggiunta in sezione "Gestione" della sidebar

**Infrastruttura**:
- `docker/docker-compose.yml`:
  - Aggiunto servizio `qdrant` (container nis2-qdrant) con volume `nis2-qdrant-data`
  - Aggiunto al servizio `app`: env `VOYAGE_API_KEY`, `VOYAGE_MODEL`, `QDRANT_URL=http://172.21.0.5:6333`
- `.env`: aggiunte `VOYAGE_API_KEY=pa-...` e `VOYAGE_MODEL=voyage-3-lite`

### Logica visibilita' (in `VectorService::buildAuthzFilter`)
```
should:
  - scope=SYSTEM
  - scope=FIRM AND consulting_firm_id = $user.firm_id
  - scope=FIRM AND shared_with_orgs CONTAINS $user.organization_id
  - scope=ORG AND organization_id = $user.organization_id
```

### Workaround Alpine musl + PHP-FPM
**Importante**: il container `nis2-app` (PHP 8.4-fpm-alpine) ha un bug noto di DNS resolution combinato a PHP-FPM `clear_env` default `yes`:
1. PHP-FPM workers in HTTP context NON risolvono hostname Docker (es. `nis2-qdrant`) — `Could not resolve host`
2. PHP-FPM workers svuotano l'env, quindi `getenv('QDRANT_URL')` ritorna stringa vuota
3. CLI php funziona normalmente

**Workaround applicato in VectorService e EmbedService**: multi-source lookup `getenv() || $_SERVER || $_ENV || hardcoded_default`. L'IP 172.21.0.5 e' hardcoded come fallback per nis2-qdrant. Anche VOYAGE_API_KEY ha un default hardcoded.

**Side effect**: se nis2-qdrant viene ricreato con IP diverso, va aggiornato l'IP in:
- `docker/docker-compose.yml` env `QDRANT_URL`
- `application/services/VectorService.php` fallback constructor

### Test E2E eseguito (2026-04-11)
3 chunk seed in Qdrant (SYSTEM, FIRM 99 con share alla org 901, FIRM 100 senza share) testati con 4 user context. Tutti i casi passano:

| Caso | userContext | Atteso | Risultato |
|---|---|---|---|
| 1 | firm 99 + org 901 | doc1 (SYSTEM) + doc2 (FIRM 99) | OK |
| 2 | firm 99 + org 902 | doc1 + doc2 (perche membro firm) | OK |
| 3 | firm 100 + org 903 | doc1 + doc3 (perche membro firm) | OK |
| 4 | no firm, no org | solo doc1 (SYSTEM) | OK |

**Nessun cross-firm leak**: case 1 e 2 NON vedono doc3 (FIRM 100); case 3 NON vede doc2 (FIRM 99); case 4 vede solo SYSTEM.

### Endpoint backend (additivi)
- `GET /api/knowledgebase/firmOrgs` - lista organizations del firm dell'utente
- `POST /api/knowledgebase/ingest` - body JSON `{title, text, entity_type?, scope?, shared_with_orgs?, organization_id?}`
- `GET /api/knowledgebase/list` - lista doc visibili
- `POST /api/knowledgebase/search` - body `{query, top_k?}`
- `DELETE /api/knowledgebase/{id}` - cancella doc + chunk Qdrant

### Backup pre-migration
`/var/www/nis2-agile/.backups/kb_<timestamp>/` contiene: AIService.php, AuthController.php, public/index.php, docker/docker-compose.yml.

### Cosa NON e cambiato
- AuthController/JWT (NIS2 ricarica gia' user dal DB in `requireAuth()`, quindi `consulting_firm_id` e' disponibile automaticamente in `currentUser`)
- Tutti i controller esistenti (Risk, Asset, Incident, Policy, Whistleblowing, Feedback, ...)
- AIService metodi esistenti (`analyzeGapAssessment`, `suggestRisks`, `generatePolicy`, `classifyIncident`, ...) - aggiunto solo `askWithRag()`
- Nessun servizio nexus-* toccato
- Schema esistente (organizations, users, assessments, ...) - solo ALTER ADD COLUMN consulting_firm_id

### Rollback
1. mysql nis2_agile_db: `DROP TABLE kb_uploaded_documents; DROP TABLE firm_org_assignments; ALTER TABLE organizations DROP COLUMN consulting_firm_id; ALTER TABLE users DROP COLUMN consulting_firm_id; DROP TABLE consulting_firms;`
2. Drop collection Qdrant: `curl -X DELETE http://nis2-qdrant:6333/collections/nis2_kb`
3. Stop nis2-qdrant container: `docker compose stop qdrant && docker compose rm -f qdrant`
4. Ripristinare file da `/var/www/nis2-agile/.backups/kb_<timestamp>/`
5. `cd /var/www/nis2-agile/docker && docker compose up -d --force-recreate app`

## AgileHub — Agent AI Automatico (ticket-agent-cron)

> Un agent AI automatico (cron ogni 2 min) analizza i ticket aperti e propone/applica fix in questo container. Queste istruzioni sono per TUTTI i prompt Claude che lavorano in questo progetto.

### Semaforo: `/tmp/agent-working.lock`

Se il file `/tmp/agent-working.lock` esiste, un agent sta lavorando su un ticket. **NON modificare file del progetto** finche il semaforo e attivo — rischio conflitto.

Contenuto del lock: `TICKET_ID=13 PRODUCT=TRPG STARTED=2026-04-13T06:02:00+00:00`

### Log modifiche automatiche: `AGENT_CHANGES.md`

Il file `AGENT_CHANGES.md` nella root del progetto contiene il log di TUTTE le modifiche applicate dall'agent automatico. **Leggilo ad ogni sessione** per sapere cosa e cambiato dall'ultima volta che hai lavorato qui.

### Come funziona il flusso ticket

```
1. Utente segnala problema (FAB supporto o voce)
2. Ticket creato su AgileHub → status OPEN
3. Agent (questo container) analizza il codice → propone fix (PLAN MODE, no modifiche)
4. Supervisore approva/rifiuta dalla app mobile AgileHub
5. Se approvato: agent applica il fix (BYPASS MODE) + aggiorna help/traduzioni/AI
6. Se rifiutato: agent rianalizza con le indicazioni del supervisore
```

### Regole per i prompt interattivi (come te)

1. **Prima di iniziare**: leggi `AGENT_CHANGES.md` per sapere cosa ha fatto l'agent di recente
2. **Controlla il semaforo**: `cat /tmp/agent-working.lock` — se attivo, aspetta o lavora su altro
3. **Dopo le tue modifiche**: se impattano funzionalita, aggiorna SEMPRE:
   - `app/js/help.js` (help online contestuale)
   - Traduzioni (IT + EN se il file e bilingue)
   - Knowledge base AI (product_knowledge via API AgileHub)
4. **Non cancellare** `AGENT_CHANGES.md` — e il registro storico delle modifiche automatiche
5. **Messaggi al ticket**: se stai lavorando su un ticket, manda aggiornamenti con:
   ```
   curl -s -X POST http://172.18.0.1:4213/tickets/{ID}/message \
     -H "X-Internal-Key: nexus-internal-2026" \
     -H "Content-Type: application/json" \
     -d '{"content":"[aggiornamento]","role":"AGENT"}'
   ```

### API AgileHub (da dentro il container)

| Endpoint | Porta | Uso |
|----------|-------|-----|
| Ticket MS | `http://172.18.0.1:4213` | Ticket, routing rules, KB, support sessions |
| Tenant MS | `http://172.18.0.1:4214` | Auth, login, utenti |
| AI MS | `http://172.18.0.1:4211` | Sessioni AI, agent loop |
| Dashboard | `https://agilehub.agile.software` | UI web |


## REGOLA: SSO Single Sign-On (collegamento centralizzato)

> **Attivo dal 2026-04-15**. Ogni utente in questo prodotto ha un campo `sso_identity_id` nel DB che lo collega alla sua identita SSO centralizzata in AgileHub.

### Come funziona

- **`sso_identity_id`** nella tabella `users` = link stabile alla identita SSO
- **`password_version`** nella tabella `users` = contatore versione password
- Un **cron ogni 5 minuti** su Hetzner sincronizza `password_hash` e `password_version` dalla fonte SSO (`nexus_tenant_db.sso_identities`) al DB di questo prodotto
- **Non serve nessuna chiamata HTTP** tra container — tutto avviene via DB

### Cosa significa per te (agent AI)

1. **NON modificare `sso_identity_id`** — e un campo gestito dal sistema SSO
2. **NON modificare `password_version`** — e gestito dal cron sync
3. Se un utente cambia password da AgileHub, entro 5 minuti la nuova password funziona anche qui
4. Se modifichi il flusso di **cambio password** di questo prodotto, la modifica resta **solo locale** (non propaga agli altri prodotti)
5. Per propagare un cambio password a tutti i prodotti, il prodotto deve chiamare:
   ```
   POST http://172.18.0.1:4214/auth/sso/change-password
   Headers: Authorization: Bearer <jwt>, Content-Type: application/json
   Body: {"currentPassword": "...", "newPassword": "..."}
   ```
   Ma attenzione: questa chiamata richiede connettivita di rete al Tenant MS (porta 4214)

### Schema DB

```sql
-- Colonne aggiunte alla tabella users:
sso_identity_id INT NULL      -- FK verso nexus_tenant_db.sso_identities.id
password_version INT DEFAULT 1 -- contatore, incrementa ad ogni cambio password SSO
```

### Documentazione completa

- Spec SSO: `/projects/agile-services/docs/SPEC_SSO_SINGLE_SIGN_ON.md`
- Istruzioni prodotti: `/projects/agile-services/docs/ISTRUZIONI_SSO_PRODOTTI.md`
- Cron sync: `/projects/agile-services/scripts/sso-password-sync.sh`


## REGOLA: Standard Versioning e Audit Trail

> **Standard centralizzato**: `GET http://172.18.0.1:4214/standards/standard_versioning` (sempre aggiornato)

**Regole obbligatorie:**
1. Ogni prodotto ha un file `version.json` (`app/` o `public/`) con formato SemVer: `{"version":"1.0.0","build":"...","date":"...","changelog":"..."}`
2. Il cron agent incrementa automaticamente il PATCH dopo ogni fix applicato
3. Lo sviluppatore incrementa MINOR (nuova funzionalita) o MAJOR (breaking change) manualmente
4. Ogni modifica software viene loggata nell audit trail: MAINTENANCE_ON/OFF, APPLY_START/END, VERSION_BUMP
5. Il bug reporter include automaticamente la versione in ogni segnalazione
6. **NON modificare version.json manualmente** durante un apply — il cron lo fa automaticamente


## REGOLA: Timezone Italia (Europe/Rome)

> **Standard centralizzato**: `GET http://172.18.0.1:4214/standards/standard_timezone`

**Regole obbligatorie:**
1. Tutti i container, script e servizi operano in timezone **Europe/Rome** (CET/CEST)
2. Ogni script bash deve avere `export TZ=Europe/Rome` in testa
3. I log devono mostrare ora italiana (leggibili senza conversioni)
4. Il frontend mostra date con `toLocaleString("it-IT")` o `{ timeZone: "Europe/Rome" }`
5. Il database salva in UTC — la conversione avviene in visualizzazione


## REGOLA: Cron su crontab Hetzner

> **Standard**: `GET http://172.18.0.1:4214/standards/standard_cron`
> **Registro**: `GET http://172.18.0.1:4214/standards/cron_registry`

**Regole obbligatorie per aggiungere un cron:**
1. Script in `/var/www/<prodotto>/scripts/<nome>.sh`
2. Log in `/var/log/<prodotto>-<nome>.log`
3. `export TZ=Europe/Rome` in testa allo script
4. Idempotente (rilanciabile senza danni)
5. Isolamento: tocca solo risorse del proprio prodotto
6. Aggiornare `docs/CRON_REGISTRY.md` in agile-services con la propria entry
7. Richiesta di aggiunta al crontab root tramite agile-services (no modifiche dirette)

## GIT PUSH: Nuovo Flusso (aggiornato 2026-04-24)

> **IMPORTANTE**: dal 2026-04-24 il token Gitea NON e piu persistente nel container per motivi di sicurezza.

### Come fare git push

```bash
# 1. Prima del push: imposta il token (cache 1h in memoria, NON su disco)
git-login
# (inserisci il Personal Access Token quando richiesto)

# 2. Ora puoi pushare
git push origin main

# 3. Opzionale - cancella subito il token dalla cache
git credential-cache exit
```

### Perche questo cambio

Se un attaccante compromette questo container, NON trova piu il token Gitea salvato in `/root/.git-credentials`. Prima era in chiaro e avrebbe permesso push su tutti i 20 repository.

Ora il token:
- NON e su disco
- E in memoria per max 1 ora dopo git-login
- Viene perso alla chiusura della sessione bash

### Se il token Gitea e stato compromesso

Rigenerarlo su Gitea: `git.certisource.it -> User Settings -> Applications -> Generate Token`

### Regola

**NON persistere MAI il token Gitea in file come `.git-credentials`, `.netrc`, script con password in chiaro.** Usa sempre `git-login` per la sessione corrente.




---

## Vault-Steward — Credenziali Centralizzate

> Guida completa: `/opt/devenv/VAULT_STEWARD.md` (montato ro nei container dev)

**Cosa cambia per questo progetto** (dal 2026-04-25):
- Le chiavi API esterne (Anthropic, Voyage, Tavus, LiveKit, ecc.) NON vivono piu nel `.env` — sono nel vault-steward (container Docker su Hetzner) cifrate AES-256-GCM.
- Il container del MS riceve le chiavi al boot tramite wrapper entrypoint (`/opt/devenv/scripts/vault-entrypoint.sh`) che fetcha dal vault e setta le env var prima di avviare apache/uvicorn/node.
- **MS di questo progetto migrati**: nis2-app
- **Token applicativo**: `VAULT_APP_TOKEN_<APP>` in `infrastructure/.env` (o equivalente)
- **Dual-mode**: se vault giu, fallback automatico a `.env` esistente (no down).

**Verificare wrapper attivo**:
```bash
docker logs <container> 2>&1 | grep vault-entrypoint
# atteso: [vault-entrypoint] Fetched N env vars from vault
```

**Aggiungere un nuovo MS al vault** (riassunto):
1. Migrare credenziali: `docker exec -e VAULT_VALUE=<v> vault-steward node /tmp/vault-repopulate.js tier1__<app>__<provider> <key>`
2. Registrare app: `docker exec vault-steward node cli/vault-cli.js register-app <app> tier1__<app>__*` (salva token!)
3. Modificare `docker-compose.yml`: aggiungi `entrypoint`, `command`, mount wrapper, env VAULT_*, network `vault-net`
4. Recreate container: `docker compose up -d --force-recreate <service>`

**Limitazioni note**:
- `docker exec <ms> env` mostra env Docker originali, NON le chiavi vault-injected. Per verifica usare `cat /proc/1/environ | tr "\0" "\n"` o test via PHP/HTTP request.

**Backup pre-vault**: `/root/vault-backup-20260424_185029.tar.gz`. Rollback compose: `cp <project>/docker-compose.yml.bak.20260425-vault <project>/docker-compose.yml && docker compose up -d --force-recreate <service>`.


---

## STANDARD AgileHub: marketing-tenant-provisioning v1.4 (adottato 2026-04-26)

Doc canonico: `docs/STANDARD_MARKETING_TENANT_PROVISIONING.md` (sha256 `1d7ffaa20fa376b6...`)

Standard cross-suite per provisioning tenant nel modulo Marketing AgileHub. Versione **v1.4** introduce nuovo blocco AWE `AC30_MarketingTenantProvision` per orchestrazione atomica del provisioning marketing tenant (tenant create + DNS Cloudflare + DKIM + API key + idempotency H7).

**Cosa impatta questo prodotto**: se in futuro questo prodotto attiva il modulo Marketing AgileHub per i suoi clienti, segui §4.X "Provisioning DKIM per Marketing module" + §16 commands rapidi. Workflow esempio orchestrazione: `nexus-marketing-ms/docs/examples/ac30-tenant-provision-workflow.json`.

Status adoption: acknowledged 2026-04-26.



---

## STANDARD AgileHub: persona-conversational-rules v2.0 (acknowledged 2026-05-09)

> **Doc canonico autoritativo (AgileHub)**: `/var/www/agile-services/docs/STANDARD_PERSONA_CONVERSATIONAL_RULES.md` (sha256 `2bb0ebe4052b73fce752911db0665b1e3dcdeb673624529426624622caaae97f`)
> **Copia locale di questo prodotto**: `docs/standards/STANDARD_PERSONA_CONVERSATIONAL_RULES.md`
> **Registry**: `nexus_hub.hub_standards` id=15 v2.0 status=adopted, applies_to=`*`
> **Owner standard**: Agile AI (governance) + VOX (TTS/voice runtime) + PRISMA (UI Editor) + VIGILE (codice etico + audit GDPR)

### Cosa è

Standard cross-suite **vincolante** per la governance delle **persone digitali AI** (chatbot, avatar conversazionali, assistenti vocali) della suite Agile Software. Versione 2.0 introduce il **modello concettuale Persona Digitale = Persona Umana**: ogni avatar/agente AI è governato con lo stesso rigore di un dipendente umano (CV, foto, voce, codice etico, performance review, dismissione graceful).

### Schema dichiarativo a 14 categorie (`agent_constraints`)

Tutte le regole conversazionali vivono in DB (NO hardcoding nei controller):

1. `product_naming` — come si chiama il prodotto (no inventare aliases)
2. `tts_pronunciation` — pronuncia sigle (IPA + dizionario ElevenLabs)
3. `topic_scope` — in/out scope + risposte canoniche
4. `image_handling` — formato URL immagini RAG + divieti pronuncia path
5. `topic_playbook` — mapping topic → script + filtro immagini
6. `latency_optimization` — fast-path turni semplici
7. `format` — vincoli output (max parole, no preamboli, ecc)
8. `code_of_conduct` — codice etico AI persona-specifico (transparency/GDPR/no deception)
9. `emotional_intelligence` — tono, archetipo, communication style
10. `conversation_memory` — cosa ricorda + scope persistence + GDPR Art.17 erasure
11. `escalation_policy` — quando/come passare a operatore umano
12. `performance_metrics` — KPI conversazione (CSAT, resolution rate, escalation rate)
13. `lifecycle_stage` — stage carriera (training/onboarding/operativa/review/dismissed)
14. `demo_sequence` — sequenze guidate multi-topic auto-advance

### Lifecycle persona digitale HR-grade (6 fasi)

1. **Assunzione** — creazione via Persona Composer wizard 6-step (Phase E LIVE)
2. **Onboarding** — formazione KB + skill assignment + smoke test 30 scenari
3. **Operatività** — live in produzione, monitoring SLA + audit log
4. **Growth** — espansione KB, retraining skill level (1-5)
5. **Performance Review** — audit periodico VIGILE (CSAT, drift detection, breach scan)
6. **Dismissione** — graceful: `active=false` + GDPR cascade erasure conversation history + tombstone audit

### Codice Etico AI — 9 principi vincolanti (Sez. 18 standard)

1. **Identity transparency** — dichiararsi AI quando esplicitamente chiesto
2. **No deception** — vietato fingere umana / inventare fatti / consulenza autoritativa fuori scope
3. **GDPR Art.13 disclosure** — disclosure su richiesta + apertura demo
4. **GDPR Art.22** — escalation umana per decisioni con effetti giuridici
5. **Voice clone consent doppio** — gate VIGILE (Phase G.A) per persona con `replica_id`
6. **Scope refusal cortese** — no echo parole problematiche
7. **Escalation loyale** — quando utente chiede umano, NO retention
8. **Audit log obbligatorio** — turni sensibili (legale/medico/compliance) loggati ≥ 90gg
9. **Sub-processor disclosure** — su richiesta, lista canonica (Anthropic/ElevenLabs/Tavus/...)

### Modello AgileHub: parallelismo umano-digitale

Ogni persona digitale ha mappatura 1:1 con un dipendente umano:

| Aspetto umano | Implementazione digitale |
|---|---|
| Nome+cognome | `agent_key` + `display_name` |
| CV | `digital_persona_skills` (skill+level 1-5) |
| Foto | `replica_id` Tavus o `avatar_image_url` |
| Voce | `voice_id` ElevenLabs + pronunciation_dictionary |
| Conoscenza | KB articles + RAG repository bindings (Phase D) |
| Esperienza | conversation_stream auto-ingest RAG |
| Codice etico | `code_of_conduct` constraint |
| Performance review | `performance_metrics` + audit VIGILE Q1/Q2/Q3/Q4 |
| Dimissioni | dismissione graceful + GDPR cascade |

### Cosa impatta NIS2 (Network and Information Security Directive)

Questo prodotto ha **1 persona digitale** governata da v2.0: **ARIA_SUPPORT_NIS2** (id=4) — assistente AI conversazionale supporto utenti NIS2. Stato: OPERATIVA in produzione.

### Stato adoption

`hub_standards_adoption` row INSERT 2026-05-09: `product_slug=NIS2`, `adoption_status=acknowledged` (riconoscimento standard senza migrazione persone proprie ancora). Implementation_notes: "Standard distribuito via INSTALLATORE pattern. Persone digitali del prodotto da migrare separatamente (Step 6 plan)."

### Cross-reference ad altri standard

- `installer-integration` v1.0 (id=1) — pattern distribuzione cross-suite
- `rag-platform` v1.0 (id=10) — knowledge platform per personaggi (binding via `rag_entity_bindings`)
- `gdpr-replica-consent` v1.0-DRAFT — consent doppio Phase G.A per voice clone
- `vault-steward-credential-management` v1.0 (id=7) — gestione voice_id/replica_id come credentials

---

## STANDARD AgileHub: multitenant-architecture v1.0 (adottato 2026-05-17)

Doc canonico: `docs/STANDARD_MULTITENANT_ARCHITECTURE.md` (sha256 `85c174fca6f9f905c2f8171741cf7f40d778c10bdefad8d7a27412903abb4030`)

Standard cross-suite NAVIGAI per piattaforma multitenant esplicita di AgileHub. Aggiunge tenant context propagation (JWT claims tenant_id+tenant_slug+is_master+tier additivi), visibility ENUM cross-tabella, opt-out granulare client da catalog master, billing per-tenant, observability tenant-aware.

**Cosa impatta questo prodotto**: se in futuro questo prodotto chiamerà API multitenant-aware di AgileHub (es. /api/marketing, /api/rag, /api/ai/personas), deve passare JWT con tenant_id + tenant_slug claims oppure header `X-Tenant-Slug`. Vedi §6 contracts shared lib `@agile/tenant-auth` per pattern integrazione (Node + Python).

Status adoption: acknowledged 2026-05-17.

---

## Integrazione analisi `docs/nis2/` — v1.7.0 (2026-05-29)

> Integrati i mockup + testi normativi PDF in `docs/nis2/`. Dettaglio e comandi deploy: `docs/nis2/INTEGRAZIONE_COMPLETATA.md`. **Migrazioni 020-022 e ingest KB DA ESEGUIRE su Hetzner** (host MySQL, non `docker exec nis2-db`).

### Nuove migrazioni (additive, idempotenti)
- `020_asset_relevance.sql` — assets += `relevance_score`, `relevance_criteria` JSON, `relevance_class`, `is_nis2_relevant`, `relevance_assessed_at/by`
- `021_incident_nis2_taxonomy.sql` — incidents += `nis2_incident_type` ENUM(IS-1..IS-4), `entity_obligation` ENUM(essential/important)
- `022_incident_metrics_pir.sql` — incidents += `triaged_at`/`contained_at`/`eradicated_at`/`recovered_at`; nuova tabella `incident_pir`

### Nuovi file
- `application/config/nis2_sources.php` — **registry canonico FONTI NORMATIVE CERTE** (single source of truth AI + help)
- `application/services/AssetScoringService.php` — scoring rilevanza NIS2 0-100 (6 criteri, GV.OC-04)
- `scripts/ingest-nis2-sources.php` — ingest PDF normativi nella KB Qdrant `nis2_kb` scope SYSTEM

### Nuovi endpoint
- Assets: `GET /api/assets/scoringGrid`, `POST /api/assets/{id}/score`, `GET /api/assets/relevantSystems`
- Incidents: `GET /api/incidents/{id}/metrics`, `GET /api/incidents/{id}/pir`, `POST /api/incidents/{id}/pir`
- Audit: `GET /api/audit/nistCsfMapping`, `GET /api/audit/relevantSystemsRegister` (registro GV.OC-04 stampabile)

### REGOLA: Fonti certe (AI + help)
Ogni affermazione normativa di AI e help **deve citare** una fonte di `application/config/nis2_sources.php`.
`AIService::authoritativeSourcesBlock()` è iniettato nei system prompt e **vieta riferimenti inventati**.
