fix: embedding background persistence, polling timeout, Error section hidden

- Embed task_id persisted in Zustand store (survives page navigation)
- Resume polling on page mount if active task exists
- Polling timeout increased to 30min (was 5min, too short for large sections)
- Embed button hidden for Error and Other sections
- Shared file-based status for multi-worker visibility
This commit is contained in:
nogueiraguh
2026-03-21 12:04:29 -03:00
parent ab2d0a3e9b
commit dcc202ff9f
2 changed files with 47 additions and 25 deletions

View File

@@ -581,7 +581,9 @@ export default function ReportsPage() {
const setEmbedAdb = store.setRptEmbedAdb; const setEmbedAdb = store.setRptEmbedAdb;
const embedTable = store.rptEmbedTable; const embedTable = store.rptEmbedTable;
const setEmbedTable = store.setRptEmbedTable; const setEmbedTable = store.setRptEmbedTable;
const [embedLoading, setEmbedLoading] = useState(''); // '' = idle, 'all' = full embed, section name = section embed const embedTaskId = store.rptEmbedTaskId;
const setEmbedTaskId = store.setRptEmbedTaskId;
const [embedLoading, setEmbedLoading] = useState('');
const [embedMsg, setEmbedMsg] = useState<{ type: 's' | 'e'; text: string } | null>(null); const [embedMsg, setEmbedMsg] = useState<{ type: 's' | 'e'; text: string } | null>(null);
const [embedProgress, setEmbedProgress] = useState<{ inserted: number; skipped: number; processed: number; total: number } | null>(null); const [embedProgress, setEmbedProgress] = useState<{ inserted: number; skipped: number; processed: number; total: number } | null>(null);
@@ -782,37 +784,53 @@ export default function ReportsPage() {
} }
}; };
const pollEmbeddingStatus = useCallback(async (taskId: string) => { const pollEmbeddingStatus = useCallback((taskId: string) => {
setEmbedMsg({ type: 's', text: 'Embedding iniciado...' }); setEmbedMsg({ type: 's', text: 'Embedding em andamento...' });
setEmbedProgress(null); setEmbedTaskId(taskId);
const poll = setInterval(async () => { const poll = setInterval(async () => {
try { try {
const s = await reportsApi.embeddingStatus(taskId); const s = await reportsApi.embeddingStatus(taskId);
if (s.total) { const data = s as unknown as Record<string, unknown>;
setEmbedProgress({ const total = (data.total as number) || 0;
inserted: s.inserted || 0, const inserted = (data.inserted as number) || 0;
skipped: (s as unknown as Record<string, number>).skipped || 0, const skipped = (data.skipped as number) || 0;
processed: (s as unknown as Record<string, number>).processed || s.inserted || 0, const processed = (data.processed as number) || inserted;
total: s.total,
}); if (total > 0) {
setEmbedProgress({ inserted, skipped, processed, total });
}
if (s.status === 'done') {
clearInterval(poll);
setEmbedProgress({ inserted, skipped, processed: total, total });
setEmbedMsg({ type: 's', text: s.message });
setTimeout(() => { setEmbedProgress(null); setEmbedLoading(''); setEmbedTaskId(''); }, 3000);
return;
}
if (s.status === 'error') {
clearInterval(poll);
setEmbedMsg({ type: 'e', text: s.message });
setTimeout(() => { setEmbedProgress(null); setEmbedLoading(''); setEmbedTaskId(''); }, 3000);
return;
} }
if (s.status === 'running') { if (s.status === 'running') {
setEmbedMsg({ type: 's', text: s.message }); setEmbedMsg({ type: 's', text: s.message });
} else if (s.status === 'done') {
clearInterval(poll);
setEmbedMsg({ type: 's', text: s.message });
setEmbedProgress(null);
setEmbedLoading('');
} else if (s.status === 'error') {
clearInterval(poll);
setEmbedMsg({ type: 'e', text: s.message });
setEmbedProgress(null);
setEmbedLoading('');
} }
} catch { /* keep polling */ } } catch {
// Network error — keep polling
}
}, 2000); }, 2000);
setTimeout(() => { clearInterval(poll); setEmbedLoading(''); setEmbedProgress(null); }, 300000); // Cleanup on unmount
}, []); return () => clearInterval(poll);
}, [setEmbedTaskId]);
// Resume polling if returning to page with active embed task
useEffect(() => {
if (!embedTaskId) return;
setEmbedLoading('all');
const cleanup = pollEmbeddingStatus(embedTaskId);
return cleanup;
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const handleEmbed = async (rid: string) => { const handleEmbed = async (rid: string) => {
const adbId = embedAdb || (adbCfg.length > 0 ? adbCfg[0].id : ''); const adbId = embedAdb || (adbCfg.length > 0 ? adbCfg[0].id : '');
@@ -1530,7 +1548,7 @@ export default function ReportsPage() {
<Icon size={13} style={{ color: 'var(--t3)', opacity: 0.7 }} /> <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-[.72rem] font-bold" style={{ color: 'var(--t1)' }}>{sec}</span>
<span className="text-[.6rem]" style={{ color: 'var(--t4)' }}>({secFiles.length})</span> <span className="text-[.6rem]" style={{ color: 'var(--t4)' }}>({secFiles.length})</span>
{adbCfg.length > 0 && secFiles.some((f) => f.file_name.endsWith('.csv')) && ( {adbCfg.length > 0 && sec !== 'Error' && sec !== 'Other' && secFiles.some((f) => f.file_name.endsWith('.csv')) && (
<button <button
onClick={() => handleEmbedSection(sec, secFiles)} onClick={() => handleEmbedSection(sec, secFiles)}
disabled={!!embedLoading} disabled={!!embedLoading}

View File

@@ -222,6 +222,8 @@ interface AppState {
rptShowCompliance: boolean; rptShowCompliance: boolean;
rptEmbedAdb: string; rptEmbedAdb: string;
rptEmbedTable: string; rptEmbedTable: string;
rptEmbedTaskId: string;
setRptEmbedTaskId: (v: string) => void;
setRptOciVal: (v: string) => void; setRptOciVal: (v: string) => void;
setRptLevel: (v: 1 | 2) => void; setRptLevel: (v: 1 | 2) => void;
setRptSelectedRegions: (v: Updater<string[]>) => void; setRptSelectedRegions: (v: Updater<string[]>) => void;
@@ -353,6 +355,8 @@ export const useAppStore = create<AppState>((set) => ({
rptShowCompliance: false, rptShowCompliance: false,
rptEmbedAdb: '', rptEmbedAdb: '',
rptEmbedTable: '', rptEmbedTable: '',
rptEmbedTaskId: '',
setRptEmbedTaskId: (v) => set({ rptEmbedTaskId: v }),
setRptOciVal: (v) => set({ rptOciVal: v }), setRptOciVal: (v) => set({ rptOciVal: v }),
setRptLevel: (v) => set({ rptLevel: v }), setRptLevel: (v) => set({ rptLevel: v }),
setRptSelectedRegions: (v) => set((s) => ({ rptSelectedRegions: typeof v === 'function' ? v(s.rptSelectedRegions) : v })), setRptSelectedRegions: (v) => set((s) => ({ rptSelectedRegions: typeof v === 'function' ? v(s.rptSelectedRegions) : v })),