Carga en lote concurrente con progreso visual: pool de 3 subidas simultaneas (PDF/audio/txt/imagenes), progreso real de bytes por archivo via XHR con panel de barras animadas, aislamiento de fallos por archivo y SQLite en modo WAL + busy_timeout para subidas concurrentes sin bloqueos. docker-compose.yaml sincronizado con docker-compose.yml

This commit is contained in:
urieljareth
2026-09-21 11:47:49 -06:00
parent 15cf00cc42
commit cd0b271bc9
5 changed files with 216 additions and 14 deletions
+39 -8
View File
@@ -45,6 +45,44 @@ async function parseResponse<T>(response: Response): Promise<T> {
return response.json() as Promise<T>;
}
export type UploadProgress = { loaded: number; total: number };
export type UploadResult = { source: Source; job: Job };
function buildUploadUrl(subjectId: number, weekNumber: number, useOcr: boolean, pageRanges?: string) {
const params = new URLSearchParams({ use_ocr: String(useOcr) });
if (pageRanges?.trim()) params.set("page_ranges", pageRanges.trim());
return `${API_URL}/subjects/${subjectId}/weeks/${weekNumber}/files?${params.toString()}`;
}
// fetch no expone progreso de subida; XHR si (xhr.upload.onprogress).
export function uploadWithProgress(
subjectId: number,
weekNumber: number,
file: File,
useOcr: boolean,
onProgress: (progress: UploadProgress) => void,
pageRanges?: string,
): Promise<UploadResult> {
return new Promise((resolve, reject) => {
const form = new FormData();
form.append("file", file);
const xhr = new XMLHttpRequest();
xhr.open("POST", buildUploadUrl(subjectId, weekNumber, useOcr, pageRanges));
xhr.upload.onprogress = (event) => {
if (event.lengthComputable && event.total > 0) onProgress({ loaded: event.loaded, total: event.total });
};
xhr.onload = () => {
let body: { detail?: string } | null = null;
try { body = JSON.parse(xhr.responseText || "null"); } catch { body = null; }
if (xhr.status >= 200 && xhr.status < 300 && body) resolve(body as UploadResult);
else reject(new Error(body?.detail || `Error ${xhr.status}`));
};
xhr.onerror = () => reject(new Error("Error de red al subir el archivo"));
xhr.onabort = () => reject(new Error("Subida cancelada"));
xhr.send(form);
});
}
export const api = {
async health() {
return parseResponse<{ status: string }>(await fetch(`${API_URL}/health`, { cache: "no-store" }));
@@ -119,14 +157,7 @@ export const api = {
return parseResponse<{ source: Source; job: Job }>(await fetch(`${API_URL}/sources/${sourceId}/reprocess?${params.toString()}`, { method: "POST" }));
},
async upload(subjectId: number, weekNumber: number, file: File, useOcr: boolean, pageRanges?: string) {
const form = new FormData();
form.append("file", file);
const params = new URLSearchParams({ use_ocr: String(useOcr) });
if (pageRanges?.trim()) params.set("page_ranges", pageRanges.trim());
return parseResponse<{ source: Source; job: Job }>(await fetch(`${API_URL}/subjects/${subjectId}/weeks/${weekNumber}/files?${params.toString()}`, {
method: "POST",
body: form,
}));
return uploadWithProgress(subjectId, weekNumber, file, useOcr, () => {}, pageRanges);
},
async markdown(documentId: number) {
const response = await fetch(`${API_URL}/documents/${documentId}/markdown`, { cache: "no-store" });