fix(parcel-sync): sync progress visible during GPKG/bundle export

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.
This commit is contained in:
AI Assistant
2026-03-07 11:23:36 +02:00
parent b0927ee075
commit 097d010b5d
3 changed files with 26 additions and 11 deletions
+11 -4
View File
@@ -196,13 +196,16 @@ export async function POST(req: Request) {
: "Sync inițial"; : "Sync inițial";
pushProgress(); pushProgress();
await syncLayer( const syncResult = await syncLayer(
validated.username, validated.username,
validated.password, validated.password,
validated.siruta, validated.siruta,
terenuriLayerId, 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); updatePhaseProgress(1, 2);
@@ -214,13 +217,16 @@ export async function POST(req: Request) {
: "Sync inițial"; : "Sync inițial";
pushProgress(); pushProgress();
await syncLayer( const syncResult = await syncLayer(
validated.username, validated.username,
validated.password, validated.password,
validated.siruta, validated.siruta,
cladiriLayerId, 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); updatePhaseProgress(2, 2);
} else { } else {
@@ -233,6 +239,7 @@ export async function POST(req: Request) {
/* ══════════════════════════════════════════════════════════ */ /* ══════════════════════════════════════════════════════════ */
/* Phase 2: Enrich (magic mode only) */ /* Phase 2: Enrich (magic mode only) */
/* ══════════════════════════════════════════════════════════ */ /* ══════════════════════════════════════════════════════════ */
// Take back progress control after syncLayer
if (validated.mode === "magic") { if (validated.mode === "magic") {
setPhaseState("Verificare îmbogățire", weights.enrich, 1); setPhaseState("Verificare îmbogățire", weights.enrich, 1);
@@ -180,13 +180,16 @@ export async function POST(req: Request) {
: "Sync inițial de la eTerra"; : "Sync inițial de la eTerra";
pushProgress(); pushProgress();
await syncLayer( const syncResult = await syncLayer(
validated.username, validated.username,
validated.password, validated.password,
validated.siruta, validated.siruta,
validated.layerId, validated.layerId,
{ forceFullSync: validated.forceSync }, { forceFullSync: validated.forceSync, jobId, isSubStep: true },
); );
if (syncResult.status === "error") {
throw new Error(syncResult.error ?? "Sync failed");
}
} else { } else {
note = "Date proaspete în baza de date — skip sync"; note = "Date proaspete în baza de date — skip sync";
pushProgress(); pushProgress();
@@ -195,6 +198,7 @@ export async function POST(req: Request) {
finishPhase(); finishPhase();
/* ── Phase 2: Build GPKG from local DB ── */ /* ── Phase 2: Build GPKG from local DB ── */
// Take back progress control after syncLayer
setPhaseState("Generare GPKG din baza de date", weights.gpkg, 1); setPhaseState("Generare GPKG din baza de date", weights.gpkg, 1);
const features = await prisma.gisFeature.findMany({ const features = await prisma.gisFeature.findMany({
@@ -50,9 +50,13 @@ export async function syncLayer(
uatName?: string; uatName?: string;
jobId?: string; jobId?: string;
forceFullSync?: boolean; 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<SyncResult> { ): Promise<SyncResult> {
const jobId = options?.jobId; const jobId = options?.jobId;
const isSubStep = options?.isSubStep ?? false;
const layer = findLayerById(layerId); const layer = findLayerById(layerId);
if (!layer) throw new Error(`Layer ${layerId} not found`); if (!layer) throw new Error(`Layer ${layerId} not found`);
@@ -261,12 +265,12 @@ export async function syncLayer(
}); });
push({ push({
phase: "Finalizat", phase: "Sync finalizat",
status: "done", status: isSubStep ? "running" : "done",
downloaded: remoteCount, downloaded: remoteCount,
total: remoteCount, total: remoteCount,
}); });
if (jobId) setTimeout(() => clearProgress(jobId), 60_000); if (jobId && !isSubStep) setTimeout(() => clearProgress(jobId), 60_000);
return { return {
layerId, layerId,
@@ -283,8 +287,8 @@ export async function syncLayer(
where: { id: syncRun.id }, where: { id: syncRun.id },
data: { status: "error", errorMessage: msg, completedAt: new Date() }, data: { status: "error", errorMessage: msg, completedAt: new Date() },
}); });
push({ phase: "Eroare", status: "error", message: msg }); push({ phase: "Eroare sync", status: isSubStep ? "running" : "error", message: msg });
if (jobId) setTimeout(() => clearProgress(jobId), 60_000); if (jobId && !isSubStep) setTimeout(() => clearProgress(jobId), 60_000);
return { return {
layerId, layerId,
siruta, siruta,