Pacchetto di design completo (nessun codice applicato, nessuna migrazione eseguita): - DESIGN aggiornato con 5 review agenti + 3 decisioni utente + pilastro AI consulente (sez. 12-14) - docs/supplier-portal/template-nis2-base.questions.json: 26 domande GV.SC (Allegato 2 ACN) con nis2_ref/vuln_flag e fonti certe verbatim - docs/supplier-portal/AI_CONSULENTE_NORMATIVO.md: corpus normativo aggiornato + persona consulente (modello TRPG) - docs/supplier-portal/UX_MINI_SPEC.md: mini-spec portale fornitore (stati/copy/autosave/mobile/a11y/editor no-code) - docs/sql/032-035: migrazioni idempotenti proposte (modulo, suppliers, portale auth, migrazione 027->campaigns) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
23 KiB
Design — Modulo Questionari Fornitori + Portale Fornitore
Stato: DESIGN DEFINITIVO da approvare (nessun codice scritto). Da rivedere prima dell'implementazione. Autore: sessione 2026-05-31. Origine: richiesta utente (Cristiano Benassati). Decisioni utente confermate: (1) categorie predefinite + personalizzabili; (2) auth fornitore via magic-link / OTP email (NO password); (3) scadenze/ricorrenze configurabili (nessun default imposto); (4) portale sullo stesso dominio
nis2.agile.software→ paginasupplier-portal.html. Base esistente riusata:SupplyChainController(self-assessment link-token, P3),supplier-assessment.html, tabellasupplier_questionnaires(mig. 027), relay email AgileHub (/api/emails/send-raw,X-Internal-Key).
1. Obiettivo
Evolvere il self-assessment fornitori (oggi: 8 domande hardcoded, link usa-e-getta) in un modulo questionari configurabile con portale fornitore ad accesso OTP, che gestisce categorie, domande estensibili (anche per procedure interne), scadenze/ricorrenze e aggiornamenti nel tempo.
Valore: due-diligence continua dei fornitori (Art. 21.2(d) NIS2; coerente con ENISA supply chain / NIST 800-161), riutilizzabile anche per procedure interne (privacy, 231, ESG, qualità).
2. Cosa esiste già (riuso)
| Esistente | Riuso nel nuovo modulo |
|---|---|
SupplyChainController::sendQuestionnaire (link-token sha256 + scadenza) |
base per generare l'accesso OTP/campagna |
publicQuestionnaire / submitPublicQuestionnaire (no-auth via token) |
base compilazione online |
supplier-assessment.html (pagina pubblica) |
diventa il portale fornitore dinamico |
relay email /api/emails/send-raw + X-Internal-Key |
invio OTP e notifiche scadenza |
pattern token sq_ + sha256 + scadenza + single-use |
riusato per OTP e accesso |
costante QUESTIONNAIRE (8 domande) |
migrata in DB come template "NIS2 base" di default |
3. Autenticazione fornitore — MAGIC-LINK / OTP (scelta utente)
Principio: nessuna password fornitore (zero credenziali da custodire/rubare). Accesso a ogni sessione tramite codice/temporale via email.
Flusso
- L'azienda registra il fornitore (anagrafica) con email referente.
- Quando serve accesso (campagna o login spontaneo), il fornitore va su
supplier-portal.htmle inserisce la propria email → riceve un magic-link (tokensp_+ 32 byte) e/o un codice OTP a 6 cifre, validi ~15 minuti, single-use. - Cliccando il link / inserendo l'OTP, si apre una sessione fornitore (token di sessione separato, durata breve, es. 2h) limitata al solo profilo di quel fornitore.
- Nel portale il fornitore vede: anagrafica propria, questionari assegnati (aperti/scaduti/completati), scadenze, storico.
Sicurezza (vincolante)
- Tabella separata
supplier_users— MAI inusersinterni. Nessun ruolo interno, nessun accesso a dati di altri fornitori o dell'organizzazione cliente. - OTP: hash in DB (mai in chiaro), scadenza 15 min, max 5 tentativi, rate-limit per email/IP, invalidazione dopo uso.
- Sessione fornitore: JWT dedicato con claim
supplier_user_id+supplier_id(NONuser_id); scope whitelisted ai soli endpoint/api/supplier-portal/*. - Enumerazione: risposta sempre "se l'email è registrata riceverai il codice" (opaca).
- Audit: login OTP, apertura/compilazione questionario loggati (
logAudit). - GDPR: l'email del referente è dato personale → retention + cancellazione su richiesta.
3.5 Caricamento anagrafica fornitori (per-azienda-cliente: CSV + API key) — requisito utente 2026-05-31
Principio: i fornitori NON si auto-registrano. L'anagrafica fornitori è di proprietà dell'azienda cliente ed è diversa per ogni org. L'azienda cliente popola la propria lista fornitori; solo dopo, il singolo referente fornitore accede via OTP/magic-link (§3) per compilare i questionari a lui assegnati.
Tre canali di ingestion (lato azienda cliente)
- Manuale — form esistente sulla scheda fornitore (
supply-chain.html). Già presente. - Import CSV/Excel — upload file → preview/mapping colonne →
bulkUpsertidempotente. Riuso del pattern già implementatoAssetController::import()+bulkUpsert()(mig. 025 assetexternal_ref/discovery_source). Dedup per(organization_id, vat_number)o(organization_id, external_ref). - API key all'azienda cliente — integrazione programmatica (es. il gestionale/ERP del cliente spinge i fornitori). Riuso dell'infra esistente
api_keys+ patternX-API-KeydiServicesController(come gli endpointingestAssets/ingestIncidentgià scritti). Nuovo scope dedicatowrite:suppliers. Endpoint upsert idempotente perexternal_ref.
Impatto schema (additivo su suppliers)
external_ref VARCHAR NULL— chiave di upsert idempotente dal sistema sorgente del cliente (comeassets.external_ref).source ENUM('manual','csv','api') DEFAULT 'manual'— provenienza del record (tracciabilità/audit).UNIQUE(organization_id, external_ref)per evitare doppioni in re-import/re-push (NULL multipli ammessi → i record manuali senza ref non confliggono).
API (lato azienda)
POST /api/supply-chain/import(JWT interno) — body CSV/JSON,bulkUpsertcon report righe ok/scartate (pattern asset import).POST /api/services/suppliers(X-API-Key, scopewrite:suppliers) — upsert idempotente perexternal_ref; un'API key è scoped alla singola org (come tutte leapi_keys), hashed, revocabile, con audit.
Sicurezza
- API key per-org, hashed in DB, scope minimo
write:suppliers, revocabile, scadenza; ogni write loggalogAudit. Mai scope cross-org. - CSV: validazione tipo/encoding, dimensione massima, sanitizzazione campi (no formula-injection in celle
=/+/-/@), dedup, nessuna esecuzione lato server. external_refè opaco dal lato cliente: non usarlo mai per costruire path/query non parametrizzate.
Distinzione chiave: questo §3.5 è il caricamento dell'anagrafica (lato azienda, API key/CSV/manuale). Il §3 è l'accesso del referente fornitore (OTP/magic-link) per compilare i questionari. Due piani separati, due meccanismi di auth separati.
4. Schema dati (migrazioni nuove, additive)
supplier_categories
id, organization_id (NULL = preset di sistema), name, slug, description, active
-- SEED predefinito (personalizzabile): Cloud/IaaS, MSP/MSSP, Software/SaaS,
-- Hardware/Manutenzione, Consulenza, Logistica, Telecomunicazioni, Altro
questionnaire_templates
id, organization_id, category_id (FK, NULL = tutte le categorie),
name, version, scope ENUM('nis2','privacy','quality','231','esg','custom'),
recurrence_months (NULL = una tantum, configurabile),
due_days_default (NULL = nessun default), is_active, created_by, created_at
-- versioning come policy_versions: snapshot a ogni pubblicazione
questionnaire_questions
id, template_id, order_index, code, text,
type ENUM('yes_no_partial','scale_1_5','single_choice','multi_choice','text','number','file'),
options JSON, weight, required, help_text
questionnaire_campaigns
id, organization_id, supplier_id, template_id, template_version,
status ENUM('draft','sent','in_progress','completed','expired','overdue'),
access_token_hash, sent_to_email, sent_at,
due_at (NULL ammesso), recurrence_months, next_recurrence_at,
reminder_offsets JSON (es. [-14,-7,-1]; configurabile), next_reminder_at, reminder_count,
score, risk_level, completed_at
questionnaire_answers
id, campaign_id, question_id, answer_value, answer_text, file_ref, answered_at
supplier_users -- account fornitore (OTP, no password)
id, supplier_id, email, display_name, status, last_login_at, created_at
supplier_otp
id, supplier_user_id, otp_hash, magic_token_hash, purpose,
expires_at, used_at, attempts, ip_created
suppliers esistente: aggiungere category_id (FK) — contact_email già presente.
5. API
Lato azienda (JWT interno)
GET/POST/PUT/DELETE /api/supply-chain/categoriesGET/POST/PUT /api/supply-chain/templates(+ versioning)GET/POST/PUT/DELETE /api/supply-chain/templates/{id}/questionsPOST /api/supply-chain/{supplierId}/campaigns(scegli template per categoria, due_at, ricorrenza, reminder)GET /api/supply-chain/campaigns(cruscotto stato/scadenze)
Lato fornitore (portale, sessione OTP — namespace dedicato /api/supplier-portal/*)
POST /api/supplier-portal/request-otp(body: email) → invia OTP/magic-link (risposta opaca)POST /api/supplier-portal/verify-otp(email + codice) → sessione fornitoreGET /api/supplier-portal/me→ anagrafica + questionari assegnati + scadenzeGET /api/supplier-portal/questionnaire/{campaignId}→ template dinamico da compilarePOST /api/supplier-portal/questionnaire/{campaignId}/submit→ salva risposte + scoring
Cron scadenze (scripts/supplier-questionnaire-cron.sh, TZ Europe/Rome)
- invia reminder agli offset configurati, marca
overdue, apre la ricorrenza alla scadenza.
6. UI
- supply-chain.html (lato azienda): tab "Categorie", "Template questionari" (editor domande ordinabile), "Campagne/scadenze" (cruscotto con semaforo in-regola/in-scadenza/scaduto). Sulla scheda fornitore: dropdown categoria + "Avvia campagna".
- supplier-portal.html (NUOVA, stesso dominio): step OTP (inserisci email → inserisci codice) → dashboard fornitore (questionari assegnati, scadenze, storico) → compilazione dinamica del template.
noindex,nofollow, nessun link all'app interna. - supplier-assessment.html: confluisce nel portale o resta come fallback link-token diretto.
7. Fasi incrementali
| Fase | Contenuto | Rischio |
|---|---|---|
| 0 (PREREQUISITO) | EmailService::sendViaRelay() via relay AgileHub (X-Internal-Key, cURL come SsoHelper::postInternal, env multi-source per clear_env FPM). Ripuntare sendQuestionnaire/forgotPassword (oggi usano mail() → falliscono in silenzio). Bloccante: senza, OTP/reminder/inviti non recapitano. |
Basso ma bloccante |
| 1 | Categorie (seed + CRUD) + template/domande configurabili in DB (migra le 8 domande) + render dinamico. Scrivere già le risposte in questionnaire_answers (campaign-stub) per non migrare in Fase 2. |
Basso |
| 2 | Campagne con scadenze/ricorrenze configurabili + reminder (cron) + cruscotto. | Medio |
| 3 | Portale fornitore + auth OTP/magic-link (supplier_users, supplier_otp, /api/supplier-portal/*, supplier-portal.html). |
Alto (auth esterna) |
| 4 | Tipi domanda avanzati (allegati/scale/multi) + scoring configurabile per peso. | Medio |
Fasi 1-2 danno già categorie + questionari configurabili + scadenze. Fase 3 aggiunge il portale OTP persistente.
8. Punti di sicurezza non negoziabili
supplier_users/sessione fornitore totalmente isolati dausersinterni (no IDOR cross-fornitore, no accesso dati org).- OTP hashed, scadenza breve, rate-limit, risposta opaca.
- Segreti/eventuali integrazioni nel vault, non in DB/Git.
- Tutti gli endpoint
/api/supplier-portal/*validano che ilcampaignId/risorsa appartenga alsupplier_iddella sessione.
9. Stima (multi-sessione)
- Fase 1: ~1 sessione · Fase 2: ~1 sessione · Fase 3 (portale+OTP): ~1.5-2 sessioni · Fase 4: ~0.5. Tutto additivo; nessuna modifica distruttiva ai moduli esistenti.
10. Decisioni APPROVATE (utente, 2026-05-31)
- ✅ Set categorie predefinite confermato (Cloud/IaaS, MSP/MSSP, Software/SaaS, Hardware/Manutenzione, Consulenza, Logistica, Telecomunicazioni, Altro) — personalizzabili.
- ✅ Auth: OTP a 6 cifre + magic-link, ENTRAMBI (il fornitore può usare l'uno o l'altro).
- ✅ Sessione fornitore = 4 ore; validità OTP/magic-link = 15 minuti, single-use, max 5 tentativi, rate-limit.
- ✅ Processo: prima review del design da 5 agenti (COMPLETATA 2026-05-31, esiti §12), poi implementazione partendo dalla Fase 0 (prerequisito email relay) → Fase 1.
- ✅ DECISO (utente, 2026-05-31, post-review):
- (a) Migrazione + sostituzione:
questionnaire_campaignssostituiscesupplier_questionnaires(027). Migrazione datiINSERT…SELECT, retrocompat dei linksq_già inviati, 027 deprecata. Una sola fonte di verità. - (b) Template "NIS2 base" completo GV.SC già in Fase 1: 8 domande attuali + domande mappate ai 5 fattori GV.SC-07 e agli ambiti GV.SC-01 §2 (con
nis2_refe fonti certe). Modulo "compliance-grade" dall'inizio. - (c) Anagrafica fornitori via manuale + CSV + API key cliente (§3.5) inclusa nel piano.
- (a) Migrazione + sostituzione:
- ✅ Contesto normativo + AI consulente (come TRPG) — vedi §13. Pilastro trasversale: corpus normativo NIS2 incluso e mantenuto aggiornato nella KB, AI istruita a rispondere come consulente NIS2 reale, sempre ancorata alle fonti certe.
11. Promemoria post-implementazione (regola progetto — VINCOLANTE per ogni fase)
Ad ogni fase che cambia funzionalità, aggiornare SEMPRE:
- Guida (
public/guida.html): nuovo capitolo/sezione "Portale fornitori" + àncora; allineare help.js_guideAnchor. - Help online (
public/js/help.js): sezione contestuale per supply-chain (questionari/portale) e per la pagina portale. - Traduzioni (
public/js/i18n.js): chiavi IT + EN per le nuove UI (categorie, template, campagne, portale, OTP). - KB AI / product_knowledge: aggiornare la knowledge base AI (RAG + product_knowledge AgileHub) con la nuova funzionalità, così l'assistente la conosce.
version.jsonbump, commit per fase, push da host.
12. Esiti review 5 agenti (2026-05-31) + correzioni al design
Verdetto comune dei 5 reviewer (sicurezza OTP, UX, architettura/DB, fattibilità stack, GDPR/supply-chain): impianto solido, NON implementabile "as-is". Le correzioni 🔴 qui sotto sono da chiudere prima della Fase 1; le ⚠️ in corso d'opera. Convergenze forti tra più agenti = priorità massima.
🔴 Bloccanti (recepiti nel design)
- Fase 0 — Email relay (fattibilità):
EmailService::send()usamail()(rotto nel container). AggiungeresendViaRelay()via AgileHubPOST /api/emails/sendconX-Internal-Key, env multi-source (getenv()?:$_SERVER?:$_ENV?:defaultper clear_env FPM). L'OTP va inviato via template/api/emails/send(NONsend-raw) così il codice non finisce nei logemail_log(convergenza sicurezza+fattibilità). - Auth fornitore = libreria SEPARATA (sicurezza+architettura+fattibilità, 3 agenti):
SUPPLIER_JWT_SECRETdedicato (da vault/.env), claimaud:"supplier-portal"SENZAuser_id,requireSupplierSession()proprio. MAI passare perBaseController::requireAuth()/active_sessions. Sessione revocabile via tabellasupplier_sessions(jti verificato a DB). Lockout persistente susupplier_otp.attempts+ rate-limit DB (non solo/tmp), invalidazione OTP precedenti,hash_equals. questionnaire_answersconorganization_iddenormalizzato (architettura) +UNIQUE(campaign_id, question_id)+ FKON DELETE CASCADE→ no leak cross-org/IDOR.- Schema → DDL completa (architettura): PK/FK/indici/tipi reali prima di codare. Indici sargable cron:
(status, next_reminder_at),(status, due_at).UNIQUE(organization_id, slug)categorie + sentinelorganization_id=0per i preset. - Coesistenza vs migrazione
supplier_questionnaires(027) (architettura+fattibilità):questionnaire_campaignsè superset di 027. DECISIONE NEL DESIGN (vedi §10 punto 5). Migrazioni nuove numerate 032+, idempotenti viainformation_schema(noADD COLUMN IF NOT EXISTS), applicatemysql -h localhostda Hetzner. Retrocompat tokensq_+ endpointpublic-questionnairein produzione. - Versioning template = snapshot dedicato (architettura+UX, F17): tabella
questionnaire_template_versions(template_id, organization_id, version, questions_snapshot JSON incl. weight, change_note, created_by, UNIQUE(template_id,version))comepolicy_versions. Le campagne in corso restano congelate alla loro versione. - Contenuto normativo del template "NIS2 base" (GDPR/supply-chain): coprire i 5 fattori GV.SC-07 + ambiti GV.SC-01 §2 a-q applicabili (cancellazione dati a fine fornitura, subfornitura, secure development, ruoli personale GV.SC-02); campo
nis2_refper mapping; scoring per-vulnerabilità (flag "no MFA"/"no patch"/…) oltre al totale (Art.21.3), modulato percriticality. Fonti da aggiungere anis2_sources.php: ENISA Good Practices SC + NIST SP 800-161r1. - GDPR (GDPR/supply-chain): base giuridica = legittimo interesse Art.6(1)(f) + obbligo legale (c) (LIA); informativa Art.13/14 in email OTP + landing portale (titolare=cliente, Agile=responsabile Art.28 + sub-processor disclosure); retention quantificata (purge
supplier_otp24-48h via cron; risposte/supplier_usersper durata relazione + ≥5 anni audit); endpoint cancellazione/anonimizzazione Art.17 con tombstone (no distruzione evidenze). - Immutabilità evidenze (GDPR/supply-chain):
questionnaire_answersappend-only + log submit/scoring inAuditService(hash-chain); ogni campagna conserva il proprio snapshot risposte+score (no sovrascrittura); soft-delete fornitore (deleted_at, no hardDatabase::delete). - Salvataggio parziale (autosave) (UX, F11 — priorità assoluta): stato
in_progress+ endpointPATCH .../answers(autosave per-domanda condebounce), "salva bozza" vs "invia". Le risposte parziali sopravvivono a logout/scadenza sessione.
🔴/⚠️ Routing (architettura+fattibilità)
Aggiungere 'supplier-portal' => 'SupplierPortalController' a controllerMap. Modellare gli URL sulle forme che il router produce davvero (id numerico primo segmento): POST:requestOtp, POST:verifyOtp, GET:me, GET:{id} (campagna), POST:{id}/submit, PATCH:{id}/answers. La notazione {campaignId} del doc NON è riconosciuta.
⚠️ UX da chiudere nel design (mini-spec §6)
- Email invito progettata (F6): nome committente in oggetto/corpo, motivo, ~tempo, scadenza in chiaro, CTA singola, branding firm.
- Gestione "email non arriva" + errori OTP umani (F2/F3): countdown reinvio, "controlla spam", messaggi specifici allo step verifica (errato/scaduto/lockout/già usato) ognuno con via d'uscita; preservare rigenera-link manuale lato azienda.
- Magic-link primario, OTP fallback (F1) + niente doppio-consumo da prefetch mail mobile (F4): consumo solo all'apertura sessione esplicita.
- Editor template no-code (F16): nascondere slug/code/order_index/options JSON/reminder_offsets/recurrence_months/weight dietro chip/dropdown/drag + preview "come lo vede il fornitore".
- i18n portale (F20): lingua IT/EN impostabile dal cliente o selezionabile (fornitori esteri).
- Ricevuta (F22): email conferma + PDF risposte scaricabile. Multi-referente/delega (F24). Score al fornitore configurabile, default OFF (F23). Progress bar (F13). Scadenza sempre visibile (F7). Mobile-first + a11y (F10/F21): elementi nativi, ARIA, touch ≥44px,
autocomplete="one-time-code".
⚠️ Operativo (fattibilità)
Cron supplier-questionnaire-cron.sh (primo .sh del progetto): wrapper sottile + runner PHP CLI, TZ=Europe/Rome, fuori finestra DST 02:00-03:00, idempotente (UPDATE condizionato), entry in CRON_REGISTRY.md, crontab via richiesta utente. Disciplina: kill -USR2 1 dopo ogni edit PHP, semaforo /tmp/agent-working.lock, commit per fase, push da host.
Stima realistica rivista
~6-7 sessioni (vs 4): Fase 0 ~0.5 · Fase 1 ~1-1.5 · Fase 2 ~1.5 · Fase 3 ~2-2.5 · Fase 4 ~0.5-1.
13. Pilastro trasversale — Contesto normativo incluso/aggiornato + AI "vero consulente" (richiesta utente 2026-05-31, modello TRPG)
Obiettivo: NIS2 Agile deve comportarsi come un consulente NIS2 reale — conoscere e citare la normativa vigente (NIS2, D.Lgs. 138/2024, Determine/Allegati ACN, ENISA, NIST), tenerla aggiornata, e rispondere sempre ancorata a fonti certe (mai inventate). Allineamento al pattern già in uso su TRPG.
Cosa esiste già in NIS2 (da censire e potenziare):
- RAG completo: Qdrant
nis2_kb(512-dim Voyage),VectorService/EmbedService/RagService,AIService::askWithRag(). - Registro fonti certe
application/config/nis2_sources.php+AIService::authoritativeSourcesBlock()(vieta riferimenti inventati). - Ingest corpus normativo:
scripts/ingest-nis2-sources.php(PDF normativi →nis2_kbscope SYSTEM). - Corpus locale:
docs/nis2/(direttiva ITA, allegati ACN 1-4, ecc.).
Gap da colmare (output dell'agente di design dedicato):
- Completezza corpus: verificare che TUTTE le fonti di
nis2_sources.phpsiano effettivamente ingerite innis2_kb; aggiungere ENISA Good Practices SC e NIST SP 800-161r1 (citati nel design ma assenti dal registro — F-12). - Aggiornamento: meccanismo per mantenere il corpus aggiornato (nuove Determine ACN, feed normativo già presente come
normative_updates) → ingest incrementale + tracciamento versione fonte. - AI "consulente": rafforzare i system prompt (persona consulente NIS2, tono, scope, citazione obbligatoria fonti) coerente con lo standard
persona-conversational-rules v2.0(persona ARIA_SUPPORT_NIS2 già esistente). Ogni risposta normativa cita §/articolo della fonte. - Copertura supply-chain: il consulente deve saper rispondere su GV.SC-01..07, art.21.2(d)/21.3, e guidare la compilazione dei questionari fornitori (collega §7 fasi a questo pilastro).
Nota: questo pilastro è parzialmente indipendente dal modulo questionari ma li potenzia (help in-context durante la compilazione, spiegazione dei nis2_ref). Sequenziabile in parallelo alle Fasi.
14. Stato lavorazione (2026-05-31)
- ✅ Design + 5 review + decisioni utente consolidate (§10, §12, §13).
- ✅ 4 agenti di produzione-design completati e materializzati come artefatti (nessun codice applicato, nessun commit):
- (A) Contenuto template GV.SC →
docs/supplier-portal/template-nis2-base.questions.json(26 domande,nis2_ref/vuln_flag, fonti certe verbatim). - (B) Pilastro AI consulente + corpus →
docs/supplier-portal/AI_CONSULENTE_NORMATIVO.md. - (C) DDL migrazioni →
docs/sql/032_supplier_questionnaires_module.sql,033_suppliers_category_source.sql,034_supplier_portal_auth.sql,035_migrate_027_to_campaigns.sql(idempotenti, NON applicate). - (D) Mini-spec UX §6 →
docs/supplier-portal/UX_MINI_SPEC.md.
- (A) Contenuto template GV.SC →
- ⏳ Prossimo (su conferma utente): commit del pacchetto design su Gitea → implementazione Fase 0 (email relay) → Fase 1 (categorie + template GV.SC + import CSV/API + render dinamico), con
kill -USR2 1/smoke/commit per fase e aggiornamento guida/help.js/i18n/KB AI.
Roadmap implementazione consolidata (ordine)
| # | Fase | Output | Migrazioni | Rischio |
|---|---|---|---|---|
| 0 | Email relay | EmailService::sendViaRelay() + ripuntare sendQuestionnaire/forgotPassword |
— | Basso (sblocca + fixa bug prod) |
| 1 | Categorie + template GV.SC + anagrafica (CSV/API) + render | seed 10 categorie + template NIS2 base (26 dom.) + import suppliers | 032, 033 | Basso |
| 2 | Campagne + scadenze/ricorrenze + reminder cron + cruscotto | — | (035 migrazione 027) | Medio |
| 3 | Portale fornitore + auth OTP/magic-link separata + sessioni revocabili | — | 034 | Alto (auth esterna) |
| 4 | Tipi domanda avanzati (file/scale/multi) + scoring per-vulnerabilità + ricevuta PDF | — | — | Medio |
| ‖ | Pilastro AI consulente (parallelo): corpus completo + persona + feed→KB + AI in-context | — | (KB su Hetzner) | Medio |