Bug CRITICO da test multi-agente: kb.js::getJwt() leggeva localStorage 'access_token' ma l'app salva il JWT sotto 'nis2_access_token' -> ogni chiamata KB inviava Authorization: Bearer (vuoto) -> 401 -> pagina KB completamente inutilizzabile (upload/list/search/delete). Stesso pattern del bug this.delete. Inoltre kb.html non aveva il blocco init (checkAuth/loadSidebar/I18n.init) presente in tutte le altre pagine -> sidebar vuota e nessun redirect a login. Fix: kb.js usa nis2_access_token; kb.html aggiunge i18n.js + init auth/chrome. node --check OK. version 1.10.6. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
231 lines
10 KiB
JavaScript
231 lines
10 KiB
JavaScript
/**
|
|
* NIS2 Agile - Knowledge Base UI (Migration 012-014)
|
|
*
|
|
* Gestisce upload documento, listing, search semantica con visibilita' multi-livello.
|
|
*/
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
function getJwt() {
|
|
try {
|
|
return localStorage.getItem('nis2_access_token') || sessionStorage.getItem('nis2_access_token') || '';
|
|
} catch (e) { return ''; }
|
|
}
|
|
|
|
function authHeaders() {
|
|
return {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer ' + getJwt(),
|
|
};
|
|
}
|
|
|
|
function escHtml(s) {
|
|
var d = document.createElement('div');
|
|
d.textContent = s == null ? '' : String(s);
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function fmtScopeBadge(scope) {
|
|
return '<span class="kb-scope-badge ' + escHtml(scope) + '">' + escHtml(scope) + '</span>';
|
|
}
|
|
|
|
var btnUpload = document.getElementById('btn-kb-upload');
|
|
var btnSubmit = document.getElementById('btn-kb-submit');
|
|
var btnCancel = document.getElementById('btn-kb-cancel');
|
|
var btnSearch = document.getElementById('btn-kb-search');
|
|
var formCard = document.getElementById('kb-form-card');
|
|
var statusEl = document.getElementById('kb-status');
|
|
var listEl = document.getElementById('kb-doc-list');
|
|
var resultsEl = document.getElementById('kb-search-results');
|
|
var shareSel = document.getElementById('kb-shared-with');
|
|
|
|
if (!btnUpload || !formCard) return;
|
|
|
|
function setupScopeForUser() {
|
|
// Decodifica JWT per estrarre role e consulting_firm_id
|
|
var jwt = getJwt();
|
|
if (!jwt) return;
|
|
try {
|
|
var payload = JSON.parse(atob(jwt.split('.')[1]));
|
|
// Il JWT NIS2 contiene solo user_id; chiamo /api/auth/me per role + firm
|
|
fetch('/api/auth/me', { headers: authHeaders() })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (res) {
|
|
var u = (res && res.data && (res.data.user || res.data)) || {};
|
|
var role = u.role || '';
|
|
var firmId = u.consulting_firm_id || null;
|
|
|
|
var optSystem = document.querySelector('[data-scope-opt="SYSTEM"]');
|
|
var optFirm = document.querySelector('[data-scope-opt="FIRM"]');
|
|
var optOrg = document.querySelector('[data-scope-opt="ORG"]');
|
|
|
|
if (role === 'super_admin' && optSystem) optSystem.style.display = 'inline-flex';
|
|
if (firmId && optFirm) optFirm.style.display = 'inline-flex';
|
|
if (optOrg) optOrg.style.display = 'inline-flex';
|
|
|
|
if (firmId) loadFirmOrgs();
|
|
});
|
|
} catch (e) { /* ignore */ }
|
|
}
|
|
|
|
function loadFirmOrgs() {
|
|
if (!shareSel) return;
|
|
fetch('/api/knowledgebase/firmOrgs', { headers: authHeaders() })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (res) {
|
|
var orgs = (res && res.data && res.data.organizations) || [];
|
|
if (!orgs.length) {
|
|
shareSel.innerHTML = '<option value="">Nessuna organizzazione nello studio</option>';
|
|
return;
|
|
}
|
|
shareSel.innerHTML = orgs.map(function (o) {
|
|
var label = o.name + (o.vat_number ? ' (P.IVA ' + o.vat_number + ')' : '');
|
|
return '<option value="' + o.id + '">' + escHtml(label) + '</option>';
|
|
}).join('');
|
|
}).catch(function () {
|
|
shareSel.innerHTML = '<option value="">Errore caricamento organizzazioni</option>';
|
|
});
|
|
}
|
|
|
|
function loadDocList() {
|
|
if (!listEl) return;
|
|
listEl.innerHTML = '<div class="text-muted">Caricamento...</div>';
|
|
fetch('/api/knowledgebase/list', { headers: authHeaders() })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (res) {
|
|
var docs = (res && res.data && res.data.documents) || [];
|
|
if (!docs.length) {
|
|
listEl.innerHTML = '<div class="text-muted">Nessun documento visibile</div>';
|
|
return;
|
|
}
|
|
listEl.innerHTML = docs.map(function (d) {
|
|
return '<div class="kb-doc-row">' +
|
|
'<div>' +
|
|
'<strong>' + escHtml(d.title) + '</strong> ' + fmtScopeBadge(d.scope) +
|
|
'<div style="font-size:.78rem;color:var(--gray-500);">' +
|
|
escHtml(d.entity_type || '') + ' · ' + (d.chunk_count || 0) + ' chunk · ' + escHtml(d.created_at) +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<button class="btn btn-sm btn-danger" data-del-id="' + d.id + '">Elimina</button>' +
|
|
'</div>';
|
|
}).join('');
|
|
// Wire delete buttons
|
|
listEl.querySelectorAll('[data-del-id]').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
if (!confirm('Eliminare definitivamente questo documento dalla KB?')) return;
|
|
var id = this.getAttribute('data-del-id');
|
|
fetch('/api/knowledgebase/' + id, {
|
|
method: 'DELETE',
|
|
headers: authHeaders(),
|
|
}).then(function (r) { return r.json(); }).then(function (res) {
|
|
if (res && res.success) loadDocList();
|
|
else alert('Errore: ' + ((res && res.message) || 'sconosciuto'));
|
|
});
|
|
});
|
|
});
|
|
}).catch(function () {
|
|
listEl.innerHTML = '<div class="text-danger">Errore caricamento</div>';
|
|
});
|
|
}
|
|
|
|
btnUpload.addEventListener('click', function () {
|
|
var visible = formCard.style.display !== 'none';
|
|
formCard.style.display = visible ? 'none' : 'block';
|
|
if (!visible) setupScopeForUser();
|
|
});
|
|
|
|
btnCancel.addEventListener('click', function () { formCard.style.display = 'none'; });
|
|
|
|
document.querySelectorAll('input[name="kb-scope"]').forEach(function (r) {
|
|
r.addEventListener('change', function () {
|
|
var block = document.getElementById('kb-share-block');
|
|
if (block) block.style.display = (this.value === 'FIRM') ? 'block' : 'none';
|
|
});
|
|
});
|
|
|
|
btnSubmit.addEventListener('click', function () {
|
|
var title = (document.getElementById('kb-title') || {}).value || '';
|
|
var text = (document.getElementById('kb-text') || {}).value || '';
|
|
var entityType = (document.getElementById('kb-entity-type') || {}).value || 'custom';
|
|
var scopeEl = document.querySelector('input[name="kb-scope"]:checked');
|
|
var scope = scopeEl ? scopeEl.value : 'ORG';
|
|
|
|
if (!title.trim()) { statusEl.textContent = 'Inserisci un titolo'; statusEl.style.color = '#ef4444'; return; }
|
|
if (text.length < 50) { statusEl.textContent = 'Testo troppo breve (min 50 caratteri)'; statusEl.style.color = '#ef4444'; return; }
|
|
|
|
var sharedWith = [];
|
|
if (scope === 'FIRM' && shareSel) {
|
|
for (var i = 0; i < shareSel.options.length; i++) {
|
|
if (shareSel.options[i].selected && shareSel.options[i].value) {
|
|
sharedWith.push(parseInt(shareSel.options[i].value, 10));
|
|
}
|
|
}
|
|
}
|
|
|
|
btnSubmit.disabled = true;
|
|
statusEl.style.color = '#0ea5e9';
|
|
statusEl.textContent = 'Indicizzazione in corso...';
|
|
|
|
fetch('/api/knowledgebase/ingest', {
|
|
method: 'POST',
|
|
headers: authHeaders(),
|
|
body: JSON.stringify({
|
|
title: title.trim(),
|
|
text: text,
|
|
entity_type: entityType,
|
|
scope: scope,
|
|
shared_with_orgs: sharedWith,
|
|
}),
|
|
}).then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
.then(function (res) {
|
|
btnSubmit.disabled = false;
|
|
if (res.ok && res.body && res.body.success !== false) {
|
|
var d = res.body.data || res.body;
|
|
statusEl.style.color = '#22C55E';
|
|
statusEl.textContent = 'Documento indicizzato (' + (d.scope || scope) + ', ' + (d.chunks || '?') + ' chunk)' +
|
|
(sharedWith.length ? ' — condiviso con ' + sharedWith.length + ' org.' : '');
|
|
document.getElementById('kb-title').value = '';
|
|
document.getElementById('kb-text').value = '';
|
|
loadDocList();
|
|
} else {
|
|
statusEl.style.color = '#ef4444';
|
|
statusEl.textContent = 'Errore: ' + ((res.body && res.body.message) || 'sconosciuto');
|
|
}
|
|
}).catch(function (e) {
|
|
btnSubmit.disabled = false;
|
|
statusEl.style.color = '#ef4444';
|
|
statusEl.textContent = 'Errore di rete: ' + (e.message || '');
|
|
});
|
|
});
|
|
|
|
btnSearch.addEventListener('click', function () {
|
|
var q = (document.getElementById('kb-search-query') || {}).value || '';
|
|
if (!q.trim()) return;
|
|
resultsEl.innerHTML = '<div class="text-muted">Ricerca in corso...</div>';
|
|
fetch('/api/knowledgebase/search', {
|
|
method: 'POST',
|
|
headers: authHeaders(),
|
|
body: JSON.stringify({ query: q.trim(), top_k: 5 }),
|
|
}).then(function (r) { return r.json(); })
|
|
.then(function (res) {
|
|
var hits = (res && res.data && res.data.results) || [];
|
|
if (!hits.length) {
|
|
resultsEl.innerHTML = '<div class="text-muted">Nessun risultato</div>';
|
|
return;
|
|
}
|
|
resultsEl.innerHTML = hits.map(function (h, i) {
|
|
return '<div style="padding:10px;border:1px solid var(--gray-100);border-radius:6px;margin-bottom:8px;">' +
|
|
'<div><strong>[' + (i + 1) + '] ' + escHtml(h.title) + '</strong> ' +
|
|
fmtScopeBadge(h.scope) + ' <span style="font-size:.72rem;color:var(--gray-500);">score=' + h.score + '</span></div>' +
|
|
'<div style="font-size:.84rem;color:var(--gray-700);margin-top:6px;line-height:1.4;">' +
|
|
escHtml(h.content.substring(0, 300)) + (h.content.length > 300 ? '...' : '') +
|
|
'</div></div>';
|
|
}).join('');
|
|
});
|
|
});
|
|
|
|
// Carica lista al boot
|
|
loadDocList();
|
|
})();
|