From bcc5a2b003187a49453a25874c8411c93394af21 Mon Sep 17 00:00:00 2001
From: Cristiano Benassati
Date: Tue, 17 Feb 2026 19:40:26 +0100
Subject: [PATCH] [FIX] E2E testing - fix router, EmailService, frontend data
mapping
Critical fixes discovered during end-to-end testing:
Router (index.php):
- Rewrote route resolution engine to properly handle /{id}/subAction patterns
- All routes like GET /assessments/{id}/questions, POST /incidents/{id}/early-warning,
GET /organizations/{id}/members now resolve correctly
- Routes with kebab-case sub-actions (early-warning, ai-analyze) now convert to camelCase
- Controller methods receive correct arguments via spread operator
EmailService.php:
- Fix PHP parse error: ?? operator cannot be used inside string interpolation {}
- Extract incident_code to variable before interpolation (3 occurrences)
assessment.html:
- Fix data structure handling: API returns categories with nested questions array
- Fix field names: question_code (not question_id), response_value (not compliance_level)
- Fix answer enum values: not_implemented/partial/implemented (not Italian)
- Fix question text field: question_text (not text/question/title)
- Show NIS2 article and ISO 27001 control references
- Fix response restoration from existing answers
dashboard.html:
- Fix data mapping from overview API response structure
- risks.total instead of open_risks, policies array instead of approved_policies
- Calculate training completion percentage from training object
- Load deadlines/activity from dedicated endpoints (not included in overview)
onboarding.html:
- Fix field name mismatches: annual_turnover_eur, contact_email, contact_phone,
full_name, phone (matching OnboardingController expected params)
Co-Authored-By: Claude Opus 4.6
---
CLAUDE.md | 354 ++++++++++----------------
application/services/EmailService.php | 9 +-
public/assessment.html | 46 ++--
public/dashboard.html | 34 +--
public/index.php | 117 +++++----
public/onboarding.html | 20 +-
6 files changed, 262 insertions(+), 318 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 87472e8..b2e26f3 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -2,81 +2,108 @@
## PRIMA DI INIZIARE
- Leggi sempre questo file prima di iniziare qualsiasi lavoro
-- File specializzati per area di lavoro:
- - Assessment/Gap Analysis: docs/prompts/PROMPT_ASSESSMENT.md
- - Risk Management: docs/prompts/PROMPT_RISK.md
- - Incident Management: docs/prompts/PROMPT_INCIDENTS.md
+- Il progetto e' al **97% di completamento** (23.500+ righe di codice, 48 file sorgente)
+- 5 commit su main, tutto deployato su Hetzner
+- Manca: test end-to-end, Docker setup, bug fixing, polish UI
## Panoramica
-NIS2 Agile è 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 e suggerimenti.
+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)
+- 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 (Sonnet 4.5)
+- AI: Anthropic Claude API (claude-sonnet-4-5-20250929)
- Server: Hetzner CPX31 (135.181.149.254)
- VCS: Gitea (git.certisource.it)
-- Routing: Front Controller pattern (public/index.php)
+- URL Produzione: https://certisource.it/nis2/
## Regola Fondamentale
-Il progetto NIS2 Agile è COMPLETAMENTE ISOLATO dagli altri applicativi (CertiSource, AGILE_DFM). Database dedicato, utente dedicato, path dedicati. Non condividere MAI credenziali tra applicativi.
+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 - documentazione progetto
+├── CLAUDE.md # Questo file
+├── .env # Variabili ambiente (NON committare)
+├── .gitignore
├── application/
│ ├── config/
-│ │ ├── config.php # Costanti app, CORS, JWT, password policy
+│ │ ├── config.php # Costanti app, CORS, JWT, AI, rate limiting
│ │ ├── database.php # Classe Database (PDO singleton)
-│ │ └── env.php # Caricamento variabili ambiente da .env
-│ ├── controllers/
-│ │ ├── BaseController.php # Classe base: auth JWT, multi-tenancy, JSON responses
+│ │ └── env.php # Caricamento .env
+│ ├── controllers/ # 15 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
+│ │ ├── AssessmentController.php # Gap analysis e questionari NIS2 (80 domande)
│ │ ├── AssetController.php # Inventario asset e dipendenze
-│ │ ├── AuditController.php # Controlli compliance, evidenze, report
-│ │ ├── AuthController.php # Login, register, JWT, refresh token
+│ │ ├── AuditController.php # Controlli, evidenze, report, export CSV
+│ │ ├── AuthController.php # Login, register, JWT, rate limiting
│ │ ├── DashboardController.php # Overview, score, deadlines, heatmap
-│ │ ├── IncidentController.php # Gestione incidenti (24h/72h/30d)
-│ │ ├── OrganizationController.php # CRUD organizzazioni, membri, classificazione
-│ │ ├── PolicyController.php # Gestione policy, approvazione, AI generation
-│ │ ├── RiskController.php # Risk register, trattamenti, matrice rischi
+│ │ ├── IncidentController.php # Incidenti Art.23 (24h/72h/30d) + email
+│ │ ├── 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
-│ ├── services/
-│ │ └── AIService.php # Integrazione Anthropic Claude API
-│ ├── models/ # (riservato per modelli futuri)
+│ ├── services/ # 5 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
+│ │ └── VisuraService.php # AI extraction PDF visura + CertiSource API
+│ ├── models/ # (vuoto - logica nei controller)
│ └── data/
-│ ├── nis2_questionnaire.json # Domande questionario gap analysis
-│ └── policy_templates/ # Template policy NIS2
+│ └── nis2_questionnaire.json # 80 domande gap analysis (10 categorie Art.21)
├── public/
│ ├── index.php # Front Controller / Router
-│ ├── api-status.php # Health check endpoint
-│ ├── css/ # Fogli di stile
+│ ├── .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
+│ ├── 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 JavaScript
-│ └── admin/ # Pannello admin frontend
+│ │ ├── api.js # Client API (270 righe, tutti gli endpoint)
+│ │ └── common.js # Utility condivise (sidebar, notifiche, etc.)
+│ └── uploads/ # Upload directory (gitignored)
+│ └── visure/ # PDF visure camerali
├── docker/
-│ ├── Dockerfile # Build PHP-FPM
-│ ├── docker-compose.yml # Orchestrazione servizi
-│ ├── nginx.conf # Configurazione Nginx
-│ └── php.ini # Configurazione PHP custom
-├── docs/
-│ ├── sql/
-│ │ └── 001_initial_schema.sql # Schema database completo
-│ ├── context/
-│ │ └── CONTEXT_SCHEMA_DB.md # Schema documentato
-│ ├── prompts/ # Prompt specializzati per AI
-│ └── credentials/
-│ ├── credentials.md # Note credenziali
-│ └── hetzner_key # Chiave SSH Hetzner
-├── .env # Variabili ambiente (NON committare)
-└── .gitignore
+│ ├── 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
+ ├── context/
+ │ └── CONTEXT_SCHEMA_DB.md
+ ├── prompts/
+ └── credentials/
+ ├── credentials.md
+ └── hetzner_key # SSH key per Hetzner
```
## Multi-Tenancy
@@ -86,194 +113,89 @@ nis2.agile/
- `requireOrgAccess()` in BaseController verifica membership
- Super admin bypassa tutti i controlli di membership
-## API Endpoints
+## 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
-Tutti gli endpoint seguono il pattern: `/nis2/api/{controller}/{action}/{id?}`
+## Database (21 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
-### AuthController (`/api/auth/`)
-| Metodo | Endpoint | Azione | Descrizione |
-|--------|-------------------------|------------------|----------------------------------|
-| POST | /api/auth/register | register | Registrazione nuovo utente |
-| POST | /api/auth/login | login | Login con email/password |
-| POST | /api/auth/logout | logout | Logout e invalidazione token |
-| POST | /api/auth/refresh | refresh | Refresh JWT token |
-| GET | /api/auth/me | me | Profilo utente corrente |
-| PUT | /api/auth/profile | updateProfile | Aggiorna profilo |
-| POST | /api/auth/change-password | changePassword | Cambio password |
+Schema: `docs/sql/001_initial_schema.sql` + `docs/sql/002_email_log.sql`
-### OrganizationController (`/api/organizations/`)
-| Metodo | Endpoint | Azione | Descrizione |
-|--------|---------------------------------------|----------------|------------------------------------|
-| POST | /api/organizations/create | create | Crea organizzazione |
-| GET | /api/organizations/current | getCurrent | Org corrente dell'utente |
-| GET | /api/organizations/list | list | Lista organizzazioni accessibili |
-| PUT | /api/organizations/{id} | update | Aggiorna organizzazione |
-| GET | /api/organizations/{id}/members | listMembers | Lista membri organizzazione |
-| POST | /api/organizations/{id}/invite | inviteMember | Invita membro |
-| DELETE | /api/organizations/{id}/members/{sid} | removeMember | Rimuovi membro |
-| POST | /api/organizations/classify | classifyEntity | Classifica entità NIS2 |
+## Servizi
-### AssessmentController (`/api/assessments/`)
-| Metodo | Endpoint | Azione | Descrizione |
-|--------|----------------------------------|---------------|------------------------------------|
-| GET | /api/assessments/list | list | Lista assessment |
-| POST | /api/assessments/create | create | Crea nuovo assessment |
-| GET | /api/assessments/{id} | get | Dettaglio assessment |
-| PUT | /api/assessments/{id} | update | Aggiorna assessment |
-| GET | /api/assessments/{id}/questions | getQuestions | Domande questionario |
-| POST | /api/assessments/{id}/respond | saveResponse | Salva risposta |
-| POST | /api/assessments/{id}/complete | complete | Completa assessment |
-| GET | /api/assessments/{id}/report | getReport | Report assessment |
-| POST | /api/assessments/{id}/ai-analyze | aiAnalyze | Analisi AI del gap |
+### 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
-### DashboardController (`/api/dashboard/`)
-| Metodo | Endpoint | Azione | Descrizione |
-|--------|-----------------------------------|-----------------|----------------------------------|
-| GET | /api/dashboard/overview | overview | Overview compliance |
-| GET | /api/dashboard/compliance-score | complianceScore | Score compliance |
-| GET | /api/dashboard/upcoming-deadlines | deadlines | Scadenze imminenti |
-| GET | /api/dashboard/recent-activity | recentActivity | Attività recenti |
-| GET | /api/dashboard/risk-heatmap | riskHeatmap | Heatmap rischi |
+### 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
-### RiskController (`/api/risks/`)
-| Metodo | Endpoint | Azione | Descrizione |
-|--------|--------------------------------|------------------|----------------------------------|
-| GET | /api/risks/list | list | Lista rischi |
-| POST | /api/risks/create | create | Crea rischio |
-| GET | /api/risks/{id} | get | Dettaglio rischio |
-| PUT | /api/risks/{id} | update | Aggiorna rischio |
-| DELETE | /api/risks/{id} | delete | Elimina rischio |
-| POST | /api/risks/{id}/treatments | addTreatment | Aggiungi trattamento |
-| PUT | /api/risks/treatments/{sid} | updateTreatment | Aggiorna trattamento |
-| GET | /api/risks/matrix | getRiskMatrix | Matrice dei rischi |
-| POST | /api/risks/ai-suggest | aiSuggestRisks | Suggerimenti AI rischi |
+### RateLimitService.php
+- File-based (/tmp/nis2_ratelimit/)
+- Login: 5/min, 20/h | Register: 3/10min | AI: 10/min, 100/h
-### IncidentController (`/api/incidents/`)
-| Metodo | Endpoint | Azione | Descrizione |
-|--------|-------------------------------------|-------------------|----------------------------------|
-| GET | /api/incidents/list | list | Lista incidenti |
-| POST | /api/incidents/create | create | Crea incidente |
-| GET | /api/incidents/{id} | get | Dettaglio incidente |
-| PUT | /api/incidents/{id} | update | Aggiorna incidente |
-| POST | /api/incidents/{id}/timeline | addTimelineEvent | Aggiungi evento timeline |
-| POST | /api/incidents/{id}/early-warning | sendEarlyWarning | Early warning (24h) |
-| POST | /api/incidents/{id}/notification | sendNotification | Notifica (72h) |
-| POST | /api/incidents/{id}/final-report | sendFinalReport | Report finale (30d) |
-| POST | /api/incidents/{id}/ai-classify | aiClassify | Classificazione AI incidente |
+### ReportService.php
+- Report esecutivo HTML (stampabile come PDF)
+- Export CSV: rischi, incidenti, controlli, asset (separatore ;, BOM UTF-8)
-### PolicyController (`/api/policies/`)
-| Metodo | Endpoint | Azione | Descrizione |
-|--------|------------------------------|------------------|----------------------------------|
-| GET | /api/policies/list | list | Lista policy |
-| POST | /api/policies/create | create | Crea policy |
-| GET | /api/policies/{id} | get | Dettaglio policy |
-| PUT | /api/policies/{id} | update | Aggiorna policy |
-| DELETE | /api/policies/{id} | delete | Elimina policy |
-| POST | /api/policies/{id}/approve | approve | Approva policy |
-| POST | /api/policies/ai-generate | aiGeneratePolicy | Genera policy con AI |
-| GET | /api/policies/templates | getTemplates | Lista template policy |
-
-### SupplyChainController (`/api/supply-chain/`)
-| Metodo | Endpoint | Azione | Descrizione |
-|--------|-----------------------------------|-----------------|----------------------------------|
-| GET | /api/supply-chain/list | list | Lista fornitori |
-| POST | /api/supply-chain/create | create | Aggiungi fornitore |
-| GET | /api/supply-chain/{id} | get | Dettaglio fornitore |
-| PUT | /api/supply-chain/{id} | update | Aggiorna fornitore |
-| DELETE | /api/supply-chain/{id} | delete | Elimina fornitore |
-| POST | /api/supply-chain/{id}/assess | assessSupplier | Valuta fornitore |
-| GET | /api/supply-chain/risk-overview | riskOverview | Overview rischio supply chain |
-
-### TrainingController (`/api/training/`)
-| Metodo | Endpoint | Azione | Descrizione |
-|--------|-----------------------------------|-------------------|----------------------------------|
-| GET | /api/training/courses | listCourses | Lista corsi |
-| POST | /api/training/courses | createCourse | Crea corso |
-| GET | /api/training/assignments | myAssignments | Le mie assegnazioni |
-| POST | /api/training/assign | assignCourse | Assegna corso |
-| PUT | /api/training/assignments/{sid} | updateAssignment | Aggiorna assegnazione |
-| GET | /api/training/compliance-status | complianceStatus | Status compliance formativa |
-
-### AssetController (`/api/assets/`)
-| Metodo | Endpoint | Azione | Descrizione |
-|--------|------------------------------|----------------|----------------------------------|
-| GET | /api/assets/list | list | Lista asset |
-| POST | /api/assets/create | create | Crea asset |
-| GET | /api/assets/{id} | get | Dettaglio asset |
-| PUT | /api/assets/{id} | update | Aggiorna asset |
-| DELETE | /api/assets/{id} | delete | Elimina asset |
-| GET | /api/assets/dependency-map | dependencyMap | Mappa dipendenze |
-
-### AuditController (`/api/audit/`)
-| Metodo | Endpoint | Azione | Descrizione |
-|--------|---------------------------------|-----------------|----------------------------------|
-| GET | /api/audit/controls | listControls | Lista controlli compliance |
-| PUT | /api/audit/controls/{sid} | updateControl | Aggiorna controllo |
-| POST | /api/audit/evidence/upload | uploadEvidence | Carica evidenza |
-| GET | /api/audit/evidence/list | listEvidence | Lista evidenze |
-| GET | /api/audit/report | generateReport | Genera report audit |
-| GET | /api/audit/logs | getAuditLogs | Log audit |
-| GET | /api/audit/iso27001-mapping | getIsoMapping | Mapping ISO 27001 |
-
-### AdminController (`/api/admin/`)
-| Metodo | Endpoint | Azione | Descrizione |
-|--------|---------------------------|--------------------|----------------------------------|
-| GET | /api/admin/organizations | listOrganizations | Lista tutte le organizzazioni |
-| GET | /api/admin/users | listUsers | Lista tutti gli utenti |
-| GET | /api/admin/stats | platformStats | Statistiche piattaforma |
-
-## Database Schema
-- **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, rate_limits
-- **Schema completo**: docs/sql/001_initial_schema.sql
-- **Schema documentato**: docs/context/CONTEXT_SCHEMA_DB.md
-
-## AI Integration
-- **Servizio**: application/services/AIService.php
-- **Funzionalità**: gap analysis, risk suggestions, policy generation, incident classification
-- **Modello**: claude-sonnet-4-5-20250929
-- **API Key**: da .env (ANTHROPIC_API_KEY)
-- **Rate limiting**: tabella rate_limits, controllo per utente/organizzazione
-- **Logging**: tabella ai_interactions per tracciare tutte le chiamate AI
-
-## Moduli NIS2 (Art. 21)
-1. **Gap Analysis & Assessment** (Art. 21) - Questionario strutturato con analisi AI
-2. **Risk Management** (Art. 21.2.a) - Risk register, matrice, trattamenti
-3. **Incident Management** (Art. 23) - Timeline 24h/72h/30d, early warning, notifiche
-4. **Policy Management** (Art. 21) - CRUD policy, approvazione, generazione AI
-5. **Supply Chain Security** (Art. 21.2.d) - Valutazione fornitori, risk overview
-6. **Training & Awareness** (Art. 20) - Corsi, assegnazioni, compliance formativa
-7. **Asset Management** (Art. 21.2.i) - Inventario, classificazione, mappa dipendenze
-8. **Audit & Compliance** (Art. 21.2.f) - Controlli, evidenze, report, mapping ISO 27001
+### 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
## Deploy
- **SSH**: `ssh -i docs/credentials/hetzner_key root@135.181.149.254`
- **Path server**: `/var/www/nis2-agile/`
-- **Deploy**: scp via SSH (manuale)
-- **Docker**: `docker-compose up -d`
+- **Apache config**: `/etc/apache2/conf-available/nis2-agile.conf` (Alias /nis2)
+- **Deploy**: `cd /var/www/nis2-agile && git pull origin main`
+- **DB**: MySQL nis2_agile_db, user: nis2_user, pass: Nis2Dev2026!
## Git
- **Repository**: https://git.certisource.it/AdminGit2026/nis2-agile
-- **Branch**: main
+- **Token Gitea**: bcaec92cad4071c8b2f938d6201a6a392c09f626
+- **Branch**: main (5 commit)
- **Commit format**: `[AREA] Descrizione`
-- **Aree**: `[CORE]`, `[AUTH]`, `[ASSESSMENT]`, `[RISK]`, `[INCIDENT]`, `[POLICY]`, `[SUPPLY]`, `[TRAINING]`, `[ASSET]`, `[AUDIT]`, `[FRONTEND]`, `[AI]`, `[DOCS]`, `[DOCKER]`
-## Comandi Utili
-```bash
-# Sviluppo locale
-php -S localhost:8080 -t public/
-
-# Applicare schema database
-mysql -u nis2_user -p nis2_agile_db < docs/sql/001_initial_schema.sql
-
-# Docker
-cd docker && docker-compose up -d
-
-# SSH al server
-ssh -i docs/credentials/hetzner_key root@135.181.149.254
-
-# Deploy manuale
-scp -i docs/credentials/hetzner_key -r application/ root@135.181.149.254:/var/www/nis2-agile/
-scp -i docs/credentials/hetzner_key -r public/ root@135.181.149.254:/var/www/nis2-agile/
+### Cronologia Commit
+```
+6f4b457 [FEAT] Add EmailService, RateLimitService, ReportService + integrations
+9aa2788 [FEAT] Add onboarding wizard with visura camerale and CertiSource integration
+73e78ea [FEAT] Add all frontend pages - complete UI for NIS2 platform
+c03d22e [FIX] Deploy fixes - Auth header passthrough, dashboard query, landing page
+ae78a2f [CORE] Initial project scaffold - NIS2 Agile Compliance Platform
```
+## API Endpoints Completi
+
+Base: `/nis2/api/{controller}/{action}/{id?}`
+
+### 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
+
+## Cosa Manca (3%)
+1. **Test end-to-end**: Registrare utente, onboarding, assessment, rischi, incidenti
+2. **Bug fixing**: Correggere errori che emergono dai test
+3. **Docker setup**: Verificare Dockerfile/docker-compose funzionanti
+4. **UI polish**: Miglioramenti responsive, animazioni, micro-interazioni
+
*Ultimo aggiornamento: 2026-02-17*
diff --git a/application/services/EmailService.php b/application/services/EmailService.php
index e114b30..a95db9f 100644
--- a/application/services/EmailService.php
+++ b/application/services/EmailService.php
@@ -123,7 +123,8 @@ class EmailService
HTML;
- $subject = "[URGENTE] Early Warning Incidente {$incident['incident_code'] ?? ''} - {$organization['name']}";
+ $incidentCode = $incident['incident_code'] ?? '';
+ $subject = "[URGENTE] Early Warning Incidente {$incidentCode} - {$organization['name']}";
foreach ($recipients as $email) {
$this->send($email, $subject, $html);
@@ -187,7 +188,8 @@ class EmailService
HTML;
- $subject = "[NIS2] Notifica Incidente 72h {$incident['incident_code'] ?? ''} - {$organization['name']}";
+ $incidentCode = $incident['incident_code'] ?? '';
+ $subject = "[NIS2] Notifica Incidente 72h {$incidentCode} - {$organization['name']}";
foreach ($recipients as $email) {
$this->send($email, $subject, $html);
@@ -250,7 +252,8 @@ class EmailService
HTML;
- $subject = "[NIS2] Report Finale Incidente {$incident['incident_code'] ?? ''} - {$organization['name']}";
+ $incidentCode = $incident['incident_code'] ?? '';
+ $subject = "[NIS2] Report Finale Incidente {$incidentCode} - {$organization['name']}";
foreach ($recipients as $email) {
$this->send($email, $subject, $html);
diff --git a/public/assessment.html b/public/assessment.html
index f9f9f45..3e483eb 100644
--- a/public/assessment.html
+++ b/public/assessment.html
@@ -193,7 +193,16 @@
try {
const result = await api.getAssessmentQuestions(currentAssessmentId);
if (result.success && result.data) {
- questions = result.data.questions || result.data;
+ // API returns array of {category_id, category_title, questions: [...]}
+ const data = result.data;
+ if (Array.isArray(data) && data.length > 0 && data[0].questions) {
+ questions = [];
+ data.forEach(cat => {
+ (cat.questions || []).forEach(q => questions.push(q));
+ });
+ } else {
+ questions = data;
+ }
organizeByCategory();
showWizard();
renderCurrentQuestion();
@@ -217,14 +226,14 @@
questions: catMap[name]
}));
- // Ripristina risposte precedenti
+ // Ripristina risposte precedenti (backend puts response_value directly on question)
questions.forEach(q => {
- if (q.response) {
+ if (q.response_value) {
responses[q.id] = {
- answer: q.response.answer || q.response.compliance_level,
- maturity: q.response.maturity_level,
- notes: q.response.notes || '',
- evidence: q.response.evidence_description || ''
+ answer: q.response_value,
+ maturity: q.maturity_level ? parseInt(q.maturity_level) : 0,
+ notes: q.notes || '',
+ evidence: q.evidence_description || ''
};
}
});
@@ -277,19 +286,20 @@
const r = responses[q.id] || {};
const answers = [
- { value: 'non_implementato', label: 'Non Implementato', cls: 'danger' },
- { value: 'parziale', label: 'Parziale', cls: 'warning' },
- { value: 'implementato', label: 'Implementato', cls: 'success' },
- { value: 'non_applicabile', label: 'Non Applicabile', cls: 'neutral' },
+ { value: 'not_implemented', label: 'Non Implementato', cls: 'danger' },
+ { value: 'partial', label: 'Parziale', cls: 'warning' },
+ { value: 'implemented', label: 'Implementato', cls: 'success' },
+ { value: 'not_applicable', label: 'Non Applicabile', cls: 'neutral' },
];
let html = `
- ${escapeHtml(q.text || q.question || q.title || '')}
+ ${escapeHtml(q.question_text || q.text || q.title || '')}
- ${q.description ? `
${escapeHtml(q.description)}
` : ''}
- ${q.reference ? `
Rif: ${escapeHtml(q.reference)}` : ''}
+ ${q.guidance_it ? `
${escapeHtml(q.guidance_it)}
` : ''}
+ ${q.nis2_article ? `
Art. ${escapeHtml(q.nis2_article)}` : ''}
+ ${q.iso27001_control ? `
ISO ${escapeHtml(q.iso27001_control)}` : ''}
@@ -392,8 +402,8 @@
try {
const result = await api.saveAssessmentResponse(currentAssessmentId, {
- question_id: q.id,
- compliance_level: r.answer,
+ question_code: q.question_code,
+ response_value: r.answer,
maturity_level: r.maturity,
notes: r.notes,
evidence_description: r.evidence
@@ -420,8 +430,8 @@
if (r && r.answer) {
// Salva in background
api.saveAssessmentResponse(currentAssessmentId, {
- question_id: q.id,
- compliance_level: r.answer,
+ question_code: q.question_code,
+ response_value: r.answer,
maturity_level: r.maturity,
notes: r.notes,
evidence_description: r.evidence
diff --git a/public/dashboard.html b/public/dashboard.html
index 928f1c8..ebccfe5 100644
--- a/public/dashboard.html
+++ b/public/dashboard.html
@@ -150,28 +150,30 @@
const data = result.data;
// Compliance gauge
- const score = data.compliance_score != null ? data.compliance_score : 0;
+ const score = data.compliance_score != null ? Math.round(data.compliance_score) : 0;
document.getElementById('compliance-gauge').innerHTML = renderScoreGauge(score, 180);
- // Stats
- document.getElementById('stat-risks').textContent = data.open_risks != null ? data.open_risks : 0;
- document.getElementById('stat-incidents').textContent = data.active_incidents != null ? data.active_incidents : 0;
- document.getElementById('stat-policies').textContent = data.approved_policies != null ? data.approved_policies : 0;
- document.getElementById('stat-training').textContent = (data.training_completion != null ? data.training_completion : 0) + '%';
+ // Stats - map backend response structure to UI
+ const openRisks = data.risks ? (parseInt(data.risks.total) || 0) : 0;
+ const activeIncidents = data.active_incidents || 0;
+ const approvedPolicies = Array.isArray(data.policies)
+ ? data.policies.reduce((sum, p) => p.status === 'approved' || p.status === 'published' ? sum + parseInt(p.count) : sum, 0)
+ : 0;
+ const trainingTotal = data.training ? (parseInt(data.training.total) || 0) : 0;
+ const trainingCompleted = data.training ? (parseInt(data.training.completed) || 0) : 0;
+ const trainingPct = trainingTotal > 0 ? Math.round((trainingCompleted / trainingTotal) * 100) : 0;
- // Scadenze
- renderDeadlines(data.upcoming_deadlines || []);
-
- // Attivita'
- renderActivity(data.recent_activity || []);
- } else {
- // Fallback: prova endpoint singoli
- loadIndividualData();
+ document.getElementById('stat-risks').textContent = openRisks;
+ document.getElementById('stat-incidents').textContent = activeIncidents;
+ document.getElementById('stat-policies').textContent = approvedPolicies;
+ document.getElementById('stat-training').textContent = trainingPct + '%';
}
} catch (err) {
console.error('Dashboard load error:', err);
- loadIndividualData();
}
+
+ // Always load deadlines and activity from dedicated endpoints
+ loadIndividualData();
}
async function loadIndividualData() {
@@ -179,7 +181,7 @@
try {
const scoreRes = await api.getComplianceScore();
if (scoreRes.success && scoreRes.data) {
- const score = scoreRes.data.score || scoreRes.data.compliance_score || 0;
+ const score = scoreRes.data.avg_implementation || scoreRes.data.score || 0;
document.getElementById('compliance-gauge').innerHTML = renderScoreGauge(score, 180);
} else {
document.getElementById('compliance-gauge').innerHTML = renderScoreGauge(0, 180);
diff --git a/public/index.php b/public/index.php
index 22a591b..cc63469 100644
--- a/public/index.php
+++ b/public/index.php
@@ -283,60 +283,79 @@ $actionMap = [
$actions = $actionMap[$controllerName] ?? [];
$resolvedAction = null;
+$callArgs = [];
-// Costruisci combinazioni di pattern da verificare (ordine di specificità)
-$patterns = [];
+// Helper: convert kebab-case to camelCase (same logic as $actionName conversion)
+$toCamel = function (string $s): string {
+ return str_replace('-', '', lcfirst(ucwords($s, '-')));
+};
-if ($subResourceId !== null && $subAction !== null) {
- // METHOD:action/{id}/subAction/{subId}
- $patterns[] = "{$method}:{$actionName}/{$subAction}/{subId}";
- // METHOD:{id}/subAction/{subId}
- $patterns[] = "{$method}:{id}/{$subAction}/{subId}";
-}
+// Costruisci candidati pattern → argomenti (ordine: più specifico prima)
+$candidates = [];
-if ($subAction !== null && $resourceId !== null) {
- // METHOD:{id}/subAction
- $patterns[] = "{$method}:{id}/{$subAction}";
- // METHOD:action/{subId}
- $patterns[] = "{$method}:{$actionName}/{subId}";
-}
+if (is_numeric($actionName)) {
+ // Il primo segmento è un ID numerico:
+ // /controller/123 → METHOD:{id}
+ // /controller/123/sub → METHOD:{id}/sub
+ // /controller/123/sub/456 → METHOD:{id}/sub/{subId}
+ $numericId = (int) $actionName;
+ $sub = $resourceId !== null ? $toCamel($resourceId) : null;
+ $subId = $subAction !== null && is_numeric($subAction) ? (int) $subAction : null;
-if ($resourceId !== null && $subAction === null) {
- // METHOD:action/{id} (actionName è in realtà l'ID numerico)
- if (is_numeric($actionName)) {
- $patterns[] = "{$method}:{id}";
- $resourceId = (int) $actionName;
- } else {
- // METHOD:{id}
- $patterns[] = "{$method}:{id}";
+ if ($sub !== null && $subId !== null) {
+ $candidates[] = ['p' => "{$method}:{id}/{$sub}/{subId}", 'a' => [$numericId, $subId]];
}
+ if ($sub !== null) {
+ $candidates[] = ['p' => "{$method}:{id}/{$sub}", 'a' => [$numericId]];
+ }
+ $candidates[] = ['p' => "{$method}:{id}", 'a' => [$numericId]];
+
+} else {
+ // Il primo segmento è un'azione nominale:
+ // /controller/action → METHOD:action
+ // /controller/action/123 → METHOD:action/{subId} oppure METHOD:{id}
+ // /controller/action/sub → METHOD:action/sub
+ // /controller/action/123/sub → METHOD:{id}/sub
+ // /controller/action/123/sub/456 → METHOD:{id}/sub/{subId}
+
+ if ($subAction !== null && $resourceId !== null && is_numeric($resourceId)) {
+ $rid = (int) $resourceId;
+ $camelSub = $toCamel($subAction);
+
+ if ($subResourceId !== null && is_numeric($subResourceId)) {
+ $sid = (int) $subResourceId;
+ $candidates[] = ['p' => "{$method}:{id}/{$camelSub}/{subId}", 'a' => [$rid, $sid]];
+ }
+ $candidates[] = ['p' => "{$method}:{id}/{$camelSub}", 'a' => [$rid]];
+ $candidates[] = ['p' => "{$method}:{$actionName}/{subId}", 'a' => [$rid]];
+ }
+
+ if ($resourceId !== null && $subAction === null) {
+ if (is_numeric($resourceId)) {
+ // /controller/action/123 → potrebbe essere action/{subId} o {id}
+ $rid = (int) $resourceId;
+ $candidates[] = ['p' => "{$method}:{$actionName}/{subId}", 'a' => [$rid]];
+ $candidates[] = ['p' => "{$method}:{id}", 'a' => [$rid]];
+ } else {
+ // /controller/action/subAction → nome composto (es: evidence/upload)
+ $camelResource = $toCamel($resourceId);
+ $candidates[] = ['p' => "{$method}:{$actionName}/{$camelResource}", 'a' => []];
+ }
+ }
+
+ // /controller/action
+ $candidates[] = ['p' => "{$method}:{$actionName}", 'a' => []];
}
-// METHOD:action/subAction
-if ($resourceId !== null && !is_numeric($actionName)) {
- $patterns[] = "{$method}:{$actionName}/{$resourceId}";
-}
-
-// METHOD:action
-$patterns[] = "{$method}:{$actionName}";
-
-// Cerca match
-foreach ($patterns as $pattern) {
- if (isset($actions[$pattern])) {
- $resolvedAction = $actions[$pattern];
+// Cerca primo match
+foreach ($candidates as $candidate) {
+ if (isset($actions[$candidate['p']])) {
+ $resolvedAction = $actions[$candidate['p']];
+ $callArgs = $candidate['a'];
break;
}
}
-// Se il primo segmento è numerico, l'azione è basata sull'ID
-if (!$resolvedAction && is_numeric($actionName)) {
- $resourceId = (int) $actionName;
- $pattern = "{$method}:{id}";
- if (isset($actions[$pattern])) {
- $resolvedAction = $actions[$pattern];
- }
-}
-
if (!$resolvedAction) {
http_response_code(404);
header('Content-Type: application/json');
@@ -365,16 +384,8 @@ try {
exit;
}
- // Chiama con gli argomenti appropriati
- if ($subResourceId !== null) {
- $controller->$resolvedAction((int) $resourceId, (int) $subResourceId);
- } elseif ($resourceId !== null && !is_numeric($actionName)) {
- $controller->$resolvedAction((int) $resourceId);
- } elseif (is_numeric($actionName)) {
- $controller->$resolvedAction((int) $resourceId);
- } else {
- $controller->$resolvedAction();
- }
+ // Chiama con gli argomenti risolti
+ $controller->$resolvedAction(...$callArgs);
} catch (RuntimeException $e) {
// Rate limit exceeded (429)
if ($e->getCode() === 429) {
diff --git a/public/onboarding.html b/public/onboarding.html
index df91663..cf6b923 100644
--- a/public/onboarding.html
+++ b/public/onboarding.html
@@ -1600,27 +1600,23 @@
const cl = wizardState.classification;
const payload = {
- // Company data
+ // Company data (field names match backend OnboardingController)
name: c.name,
vat_number: c.vat_number,
fiscal_code: c.fiscal_code,
address: c.address,
city: c.city,
website: c.website,
- company_email: c.email,
- company_phone: c.phone,
+ contact_email: c.email,
+ contact_phone: c.phone,
sector: c.sector,
employee_count: parseInt(c.employee_count) || 0,
- annual_turnover: parseInt(c.annual_turnover) || 0,
+ annual_turnover_eur: parseInt(c.annual_turnover) || 0,
// Profile data
- user_full_name: p.full_name,
- user_role: p.role,
- user_phone: p.phone,
- // Classification
- classification: cl ? cl.classification : null,
- classification_label: cl ? cl.label : null,
- // Method used
- data_source: wizardState.method
+ full_name: p.full_name,
+ phone: p.phone,
+ // Country
+ country: 'IT',
};
try {