From 097d010b5d3b219586b6b21e01158e1f369fc758 Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Sat, 7 Mar 2026 11:23:36 +0200 Subject: [PATCH] fix(parcel-sync): sync progress visible during GPKG/bundle export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3 bugs fixed: - syncLayer was called without jobId -> user saw no progress duringSync - syncLayer set status:'done' prematurely -> client stopped polling before GPKG phase - syncLayer errors were silently ignored -> confusing 'no features in DB' error Added isSubStep option to syncLayer: when true, keeps status as 'running' and doesn't schedule clearProgress. Export routes now pass jobId + isSubStep so the real sync progress (Descărcare features 50/200) is visible in the UI. --- src/app/api/eterra/export-bundle/route.ts | 15 +++++++++++---- src/app/api/eterra/export-layer-gpkg/route.ts | 8 ++++++-- src/modules/parcel-sync/services/sync-service.ts | 14 +++++++++----- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/app/api/eterra/export-bundle/route.ts b/src/app/api/eterra/export-bundle/route.ts index e70b6e0..6a2d235 100644 --- a/src/app/api/eterra/export-bundle/route.ts +++ b/src/app/api/eterra/export-bundle/route.ts @@ -196,13 +196,16 @@ export async function POST(req: Request) { : "Sync inițial"; pushProgress(); - await syncLayer( + const syncResult = await syncLayer( validated.username, validated.password, validated.siruta, terenuriLayerId, - { forceFullSync: validated.forceSync }, + { forceFullSync: validated.forceSync, jobId, isSubStep: true }, ); + if (syncResult.status === "error") { + throw new Error(syncResult.error ?? "Sync terenuri failed"); + } } updatePhaseProgress(1, 2); @@ -214,13 +217,16 @@ export async function POST(req: Request) { : "Sync inițial"; pushProgress(); - await syncLayer( + const syncResult = await syncLayer( validated.username, validated.password, validated.siruta, cladiriLayerId, - { forceFullSync: validated.forceSync }, + { forceFullSync: validated.forceSync, jobId, isSubStep: true }, ); + if (syncResult.status === "error") { + throw new Error(syncResult.error ?? "Sync clădiri failed"); + } } updatePhaseProgress(2, 2); } else { @@ -233,6 +239,7 @@ export async function POST(req: Request) { /* ══════════════════════════════════════════════════════════ */ /* Phase 2: Enrich (magic mode only) */ /* ══════════════════════════════════════════════════════════ */ + // Take back progress control after syncLayer if (validated.mode === "magic") { setPhaseState("Verificare îmbogățire", weights.enrich, 1); diff --git a/src/app/api/eterra/export-layer-gpkg/route.ts b/src/app/api/eterra/export-layer-gpkg/route.ts index ca57faf..7b2a0df 100644 --- a/src/app/api/eterra/export-layer-gpkg/route.ts +++ b/src/app/api/eterra/export-layer-gpkg/route.ts @@ -180,13 +180,16 @@ export async function POST(req: Request) { : "Sync inițial de la eTerra"; pushProgress(); - await syncLayer( + const syncResult = await syncLayer( validated.username, validated.password, validated.siruta, validated.layerId, - { forceFullSync: validated.forceSync }, + { forceFullSync: validated.forceSync, jobId, isSubStep: true }, ); + if (syncResult.status === "error") { + throw new Error(syncResult.error ?? "Sync failed"); + } } else { note = "Date proaspete în baza de date — skip sync"; pushProgress(); @@ -195,6 +198,7 @@ export async function POST(req: Request) { finishPhase(); /* ── Phase 2: Build GPKG from local DB ── */ + // Take back progress control after syncLayer setPhaseState("Generare GPKG din baza de date", weights.gpkg, 1); const features = await prisma.gisFeature.findMany({ diff --git a/src/modules/parcel-sync/services/sync-service.ts b/src/modules/parcel-sync/services/sync-service.ts index 5c6d0fd..b005efc 100644 --- a/src/modules/parcel-sync/services/sync-service.ts +++ b/src/modules/parcel-sync/services/sync-service.ts @@ -50,9 +50,13 @@ export async function syncLayer( uatName?: string; jobId?: string; forceFullSync?: boolean; + /** When true, don't set terminal status (done/error) on progress store. + * Used when syncLayer runs as a sub-step of a larger export flow. */ + isSubStep?: boolean; }, ): Promise { const jobId = options?.jobId; + const isSubStep = options?.isSubStep ?? false; const layer = findLayerById(layerId); if (!layer) throw new Error(`Layer ${layerId} not found`); @@ -261,12 +265,12 @@ export async function syncLayer( }); push({ - phase: "Finalizat", - status: "done", + phase: "Sync finalizat", + status: isSubStep ? "running" : "done", downloaded: remoteCount, total: remoteCount, }); - if (jobId) setTimeout(() => clearProgress(jobId), 60_000); + if (jobId && !isSubStep) setTimeout(() => clearProgress(jobId), 60_000); return { layerId, @@ -283,8 +287,8 @@ export async function syncLayer( where: { id: syncRun.id }, data: { status: "error", errorMessage: msg, completedAt: new Date() }, }); - push({ phase: "Eroare", status: "error", message: msg }); - if (jobId) setTimeout(() => clearProgress(jobId), 60_000); + push({ phase: "Eroare sync", status: isSubStep ? "running" : "error", message: msg }); + if (jobId && !isSubStep) setTimeout(() => clearProgress(jobId), 60_000); return { layerId, siruta,