feat: auto-mapped CIS report embedding, section embed, ADB RAW(16) fix
- Auto-embed report: maps each CSV to its ADB table (summaryreportcsvvector, identityandaccess, networking, etc.)
- Section embed: new endpoint POST /api/embeddings/report/{rid}/section with per-section button in UI
- Table validation: checks registered ADB tables before embedding, reports missing tables
- Auto-detect embedding dimension: reads table dim and selects correct model (3072→large, 1536→small)
- ADB RAW(16) ID fix: all vector inserts use HEXTORAW() for UUID (fixes ORA-01465)
- Float32 vectors: all inserts use array.array('f') for FLOAT32 compatibility
- Embedding status: real-time progress polling (Embedding X/Y — table: Z)
- Loading per section: spinner only on the section being embedded, not all
- Removed individual file embed buttons (only section + full report)
- Summary CSV chunking: groups by CIS section with tenancy + extract_date metadata
- Findings CSV chunking: each row becomes a document with structured content
- README: documented all 11 required ADB vector tables with descriptions
This commit is contained in:
@@ -581,7 +581,7 @@ export default function ReportsPage() {
|
||||
const setEmbedAdb = store.setRptEmbedAdb;
|
||||
const embedTable = store.rptEmbedTable;
|
||||
const setEmbedTable = store.setRptEmbedTable;
|
||||
const [embedLoading, setEmbedLoading] = useState(false);
|
||||
const [embedLoading, setEmbedLoading] = useState(''); // '' = idle, 'all' = full embed, section name = section embed
|
||||
const [embedMsg, setEmbedMsg] = useState<{ type: 's' | 'e'; text: string } | null>(null);
|
||||
|
||||
/* ── HTML iframe (from store) ── */
|
||||
@@ -781,45 +781,83 @@ export default function ReportsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const pollEmbeddingStatus = useCallback(async (taskId: string) => {
|
||||
setEmbedMsg({ type: 's', text: 'Embedding iniciado...' });
|
||||
const poll = setInterval(async () => {
|
||||
try {
|
||||
const s = await reportsApi.embeddingStatus(taskId);
|
||||
if (s.status === 'running') {
|
||||
setEmbedMsg({ type: 's', text: s.message });
|
||||
} else if (s.status === 'done') {
|
||||
clearInterval(poll);
|
||||
setEmbedMsg({ type: 's', text: s.message });
|
||||
setEmbedLoading('');
|
||||
} else if (s.status === 'error') {
|
||||
clearInterval(poll);
|
||||
setEmbedMsg({ type: 'e', text: s.message });
|
||||
setEmbedLoading('');
|
||||
}
|
||||
} catch { /* keep polling */ }
|
||||
}, 2000);
|
||||
setTimeout(() => { clearInterval(poll); setEmbedLoading(''); }, 300000);
|
||||
}, []);
|
||||
|
||||
const handleEmbed = async (rid: string) => {
|
||||
if (!embedAdb) {
|
||||
setEmbedMsg({ type: 'e', text: t('rpt.selectAdb') });
|
||||
return;
|
||||
}
|
||||
if (!embedTable) {
|
||||
setEmbedMsg({ type: 'e', text: t('rpt.selectTable') });
|
||||
return;
|
||||
}
|
||||
setEmbedLoading(true);
|
||||
const adbId = embedAdb || (adbCfg.length > 0 ? adbCfg[0].id : '');
|
||||
if (!adbId) { setEmbedMsg({ type: 'e', text: t('rpt.selectAdb') }); return; }
|
||||
setEmbedLoading('all');
|
||||
setEmbedMsg(null);
|
||||
try {
|
||||
const r = await reportsApi.embedReport(rid, embedAdb, embedTable);
|
||||
setEmbedMsg({ type: 's', text: r.message });
|
||||
const r = await reportsApi.embedReport(rid, adbId);
|
||||
if (r.task_id) {
|
||||
pollEmbeddingStatus(r.task_id);
|
||||
} else {
|
||||
setEmbedMsg({ type: 's', text: r.message });
|
||||
setEmbedLoading('');
|
||||
}
|
||||
} catch (err) {
|
||||
setEmbedMsg({ type: 'e', text: err instanceof Error ? err.message : t('rpt.errorEmbedding') });
|
||||
} finally {
|
||||
setEmbedLoading(false);
|
||||
setEmbedLoading('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmbedFile = async (rid: string, fid: string) => {
|
||||
if (!embedAdb) {
|
||||
setEmbedMsg({ type: 'e', text: t('rpt.selectAdb') });
|
||||
return;
|
||||
}
|
||||
if (!embedTable) {
|
||||
setEmbedMsg({ type: 'e', text: t('rpt.selectTable') });
|
||||
return;
|
||||
}
|
||||
setEmbedLoading(true);
|
||||
if (!embedAdb) { setEmbedMsg({ type: 'e', text: t('rpt.selectAdb') }); return; }
|
||||
if (!embedTable) { setEmbedMsg({ type: 'e', text: t('rpt.selectTable') }); return; }
|
||||
setEmbedLoading('all');
|
||||
setEmbedMsg(null);
|
||||
try {
|
||||
const r = await reportsApi.embedFile(rid, fid, embedAdb, embedTable);
|
||||
setEmbedMsg({ type: 's', text: r.message });
|
||||
if (r.task_id) {
|
||||
pollEmbeddingStatus(r.task_id);
|
||||
} else {
|
||||
setEmbedMsg({ type: 's', text: r.message });
|
||||
setEmbedLoading('');
|
||||
}
|
||||
} catch (err) {
|
||||
setEmbedMsg({ type: 'e', text: err instanceof Error ? err.message : t('rpt.errorEmbedding') });
|
||||
} finally {
|
||||
setEmbedLoading(false);
|
||||
setEmbedLoading('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmbedSection = async (secName: string, sectionFiles: ReportFile[]) => {
|
||||
const csvFiles = sectionFiles.filter((f) => f.file_name.endsWith('.csv'));
|
||||
if (!csvFiles.length) return;
|
||||
const adbId = embedAdb || (adbCfg.length > 0 ? adbCfg[0].id : '');
|
||||
if (!adbId) { setEmbedMsg({ type: 'e', text: t('rpt.selectAdb') }); return; }
|
||||
setEmbedLoading(secName);
|
||||
setEmbedMsg(null);
|
||||
try {
|
||||
const r = await reportsApi.embedSection(selectedRid, csvFiles.map((f) => f.file_name), adbId);
|
||||
if (r.task_id) {
|
||||
pollEmbeddingStatus(r.task_id);
|
||||
} else {
|
||||
setEmbedMsg({ type: 's', text: r.message });
|
||||
setEmbedLoading('');
|
||||
}
|
||||
} catch (err) {
|
||||
setEmbedMsg({ type: 'e', text: err instanceof Error ? err.message : t('rpt.errorEmbedding') });
|
||||
setEmbedLoading('');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1409,43 +1447,15 @@ export default function ReportsPage() {
|
||||
|
||||
{/* Embedding controls */}
|
||||
{adbCfg.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
value={embedAdb}
|
||||
onChange={(e) => {
|
||||
setEmbedAdb(e.target.value);
|
||||
const cfg = adbCfg.find((c) => c.id === e.target.value);
|
||||
const tables = (cfg?.tables || []).filter((t) => t.is_active);
|
||||
setEmbedTable(tables[0]?.table_name || '');
|
||||
}}
|
||||
className="text-[.68rem] py-1 px-2 rounded-md outline-none cursor-pointer"
|
||||
style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t1)', maxWidth: 160 }}
|
||||
>
|
||||
{adbCfg.map((c) => (
|
||||
<option key={c.id} value={c.id}>{c.config_name}</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={embedTable}
|
||||
onChange={(e) => setEmbedTable(e.target.value)}
|
||||
className="text-[.68rem] py-1 px-2 rounded-md outline-none cursor-pointer"
|
||||
style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t1)', maxWidth: 180 }}
|
||||
>
|
||||
{embedAdbTables.length === 0 && <option value="">{t('rpt.noActiveTables')}</option>}
|
||||
{embedAdbTables.map((t) => (
|
||||
<option key={t.table_name} value={t.table_name}>{t.table_name}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
onClick={() => handleEmbed(selectedRid)}
|
||||
disabled={embedLoading || !embedTable}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[.68rem] font-semibold transition-colors"
|
||||
style={{ background: 'color-mix(in srgb, var(--yl) 12%, transparent)', color: 'var(--yl)', border: '1px solid color-mix(in srgb, var(--yl) 25%, transparent)' }}
|
||||
>
|
||||
{embedLoading ? <Loader2 size={12} className="animate-spin" /> : <Dna size={12} />}
|
||||
{t('rpt.fullEmbedding')}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleEmbed(selectedRid)}
|
||||
disabled={!!embedLoading}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[.68rem] font-semibold transition-colors"
|
||||
style={{ background: 'color-mix(in srgb, var(--yl) 12%, transparent)', color: 'var(--yl)', border: '1px solid color-mix(in srgb, var(--yl) 25%, transparent)' }}
|
||||
>
|
||||
{embedLoading === 'all' ? <Loader2 size={12} className="animate-spin" /> : <Dna size={12} />}
|
||||
{t('rpt.fullEmbedding')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1481,6 +1491,18 @@ export default function ReportsPage() {
|
||||
<Icon size={13} style={{ color: 'var(--t3)', opacity: 0.7 }} />
|
||||
<span className="text-[.72rem] font-bold" style={{ color: 'var(--t1)' }}>{sec}</span>
|
||||
<span className="text-[.6rem]" style={{ color: 'var(--t4)' }}>({secFiles.length})</span>
|
||||
{adbCfg.length > 0 && secFiles.some((f) => f.file_name.endsWith('.csv')) && (
|
||||
<button
|
||||
onClick={() => handleEmbedSection(sec, secFiles)}
|
||||
disabled={!!embedLoading}
|
||||
className="ml-auto flex items-center gap-1 px-2 py-0.5 rounded-md text-[.6rem] font-semibold transition-colors cursor-pointer"
|
||||
style={{ background: 'color-mix(in srgb, var(--yl) 10%, transparent)', color: 'var(--yl)', border: '1px solid color-mix(in srgb, var(--yl) 20%, transparent)' }}
|
||||
title={`Embed ${sec} CSVs`}
|
||||
>
|
||||
{embedLoading === sec ? <Loader2 size={10} className="animate-spin" /> : <Dna size={10} />}
|
||||
Embed
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* File grid */}
|
||||
@@ -1520,17 +1542,6 @@ export default function ReportsPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{canEmbed && adbCfg.length > 0 && (
|
||||
<button
|
||||
onClick={() => handleEmbedFile(selectedRid, f.id)}
|
||||
disabled={embedLoading}
|
||||
className="p-1.5 rounded-md opacity-30 group-hover:opacity-80 transition-opacity"
|
||||
title={`Embedding: ${f.file_name}`}
|
||||
style={{ color: 'var(--yl)' }}
|
||||
>
|
||||
<Dna size={12} />
|
||||
</button>
|
||||
)}
|
||||
<a
|
||||
href={`/api/reports/${selectedRid}/files/${f.id}/download?token=${encodeURIComponent(token)}`}
|
||||
target="_blank"
|
||||
|
||||
Reference in New Issue
Block a user