# MAPPA-LOGICA · Knowledge Base ⟦ INIZIO · U-KB · Videata "Knowledge Base" ⟧ File: public/kb.html · URL: /kb.html · Scopo: caricamento e gestione dei documenti (procedure, policy, normative, training material) che l'AI usa per rispondere con citazioni precise (pipeline RAG su Qdrant), con visibilità a 3 livelli: SYSTEM (vendor), FIRM (studio + condivisioni esplicite), ORG (singola organizzazione cliente) · Profili: lettura/ricerca per tutti gli utenti autenticati (la visibilità applica il filtro authz a 3 livelli); l'upload (ingest) è riservato ai ruoli super_admin, org_admin, compliance_manager e consultant (employee/auditor read-only, 403 KB_FORBIDDEN). Per scope: SYSTEM solo super_admin; FIRM solo utenti con consulting_firm_id (consulenti di studio); ORG richiede ruolo org_admin/compliance_manager sull'organizzazione destinataria (super_admin bypassa) e `organization_id` valorizzato lato backend. La UI mostra/nasconde le radio di scope client-side (`setupScopeForUser`), ma l'enforcement è nel backend (KnowledgeBaseController::ingest). • U-KB.1 — Header pagina + "Carica documento" Mostra: titolo "Knowledge Base"; paragrafo introduttivo sul modello di visibilità a 3 livelli (SYSTEM/FIRM/ORG). Azioni: bottone "Carica documento" → toggle del form di upload (U-KB.2); all'apertura chiama `setupScopeForUser()` che mostra/nasconde le opzioni di scope in base a ruolo e firm. Dati da: GET /api/auth/me (per role e consulting_firm_id, determina gli scope disponibili). Confine: `#btn-kb-upload` (riga 34). • U-KB.2 — Form di upload/indicizzazione documento (nascosto di default) Mostra: campo "Titolo"; select "Tipo" (Procedura interna / Linea guida / Normativa / Training); textarea "Contenuto (testo libero, 50–50.000 caratteri)"; radio "Visibilità": "Sistema (super_admin)" (visibile solo a super_admin), "Studio + condivisioni" (visibile solo a utenti firm), "Solo organizzazione corrente" (default, sempre visibile); status di indicizzazione ("Indicizzazione in corso..." / "Documento indicizzato (SCOPE, N chunk)" / messaggi di errore con validazione: titolo obbligatorio, testo minimo 50 caratteri). Azioni: "Indicizza documento" → POST ingest (embed Voyage + upsert Qdrant + tracking MySQL), poi svuota i campi e ricarica la lista; "Annulla" → nasconde il form; selezione radio "FIRM" → mostra il blocco condivisione (U-KB.3). Dati da: POST /api/knowledgebase/ingest (body: title, text, entity_type, scope, shared_with_orgs[]). Confine: `#kb-form-card` (riga 50); `#kb-title` (55), `#kb-entity-type` (59), `#kb-text` (70), radio `input[name="kb-scope"]` con label `[data-scope-opt="SYSTEM|FIRM|ORG"]` (righe 77-84), `#btn-kb-submit` (102), `#btn-kb-cancel` (103), `#kb-status` (104). • U-KB.3 — Blocco condivisione FIRM (visibile solo con scope=FIRM) Mostra: multi-select "Organizzazioni clienti destinatarie (Ctrl/Cmd per selezione multipla, opzionale)" popolata con le organizzazioni dello studio (nome + P.IVA); nota: "Lascia vuoto per condividere solo con i collaboratori dello studio". Azioni: selezione multipla delle org destinatarie → inviate come shared_with_orgs nell'ingest. Dati da: GET /api/knowledgebase/firmOrgs (risposta: data.organizations[]). Confine: `#kb-share-block` (riga 89), `#kb-shared-with` (riga 93). • U-KB.4 — Lista "Documenti caricati visibili" Mostra: per ogni documento visibile all'utente: titolo, badge scope colorato (SYSTEM blu / FIRM ambra / ORG verde), tipo entità, numero chunk, data di creazione; "Nessun documento visibile" se vuota. La visibilità applica il filtro authz a 3 livelli lato backend. Azioni: bottone "Elimina" per documento → conferma → DELETE (rimuove documento + chunk da Qdrant) e ricarica la lista. Dati da: GET /api/knowledgebase/list (risposta: data.documents[]); DELETE /api/knowledgebase/{id}. Confine: card `.kb-doc-list` (riga 110); contenitore lista `#kb-doc-list` (riga 115); bottoni elimina dinamici con attributo `[data-del-id=""]` (kb.js riga 110). • U-KB.5 — "Test search semantica" Mostra: campo query (placeholder "Es: come gestire un incidente NIS2?"); risultati: per ogni hit [N] titolo, badge scope, score di similarità, estratto del contenuto (primi 300 caratteri); "Nessun risultato" se vuoto. Azioni: bottone "Cerca" → POST search semantica (top 5 risultati) nel perimetro di visibilità dell'utente. Dati da: POST /api/knowledgebase/search (body: {query, top_k: 5}; risposta: data.results[]). Confine: `#kb-search-query` (riga 127), `#btn-kb-search` (riga 128), `#kb-search-results` (riga 133). ⟦ FINE · U-KB ⟧ ## ID STABILI MANCANTI (da aggiungere) | Elemento | Riga | Id proposto | |---|---|---| | Card lista documenti (wrapper) | 110 | `nis2-kb-doc-list-card` | | Card test search (wrapper) | 122 | `nis2-kb-search-card` | | Paragrafo intro visibilità | 42 | `nis2-kb-intro` | | Bottone "Elimina" documento (dinamico) | kb.js 110 | già presente `data-del-id` (selettore stabile, ok) |