diff --git a/src/app/(modules)/monitor/page.tsx b/src/app/(modules)/monitor/page.tsx
index e7999c5..0ec04ac 100644
--- a/src/app/(modules)/monitor/page.tsx
+++ b/src/app/(modules)/monitor/page.tsx
@@ -235,6 +235,19 @@ export default function MonitorPage() {
loading={actionLoading === "warm-cache"}
onClick={triggerWarmCache}
/>
+
void;
addLog: (type: "info" | "ok" | "error" | "wait", msg: string) => void;
pollRef: React.MutableRefObject | null>;
+ customEndpoint?: string;
}) {
const startTimeRef = useRef(0);
const formatElapsed = () => {
@@ -372,12 +386,14 @@ function SyncTestButton({ label, description, siruta, mode, includeNoGeometry, a
onClick={async () => {
setActionLoading(actionKey);
startTimeRef.current = Date.now();
- addLog("info", `[${label}] Pornire sync (${mode}, noGeom=${includeNoGeometry})...`);
+ addLog("info", `[${label}] Pornire...`);
try {
- const res = await fetch("/api/eterra/sync-background", {
+ const endpoint = customEndpoint ?? "/api/eterra/sync-background";
+ const body = customEndpoint ? {} : { siruta, mode, includeNoGeometry };
+ const res = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ siruta, mode, includeNoGeometry }),
+ body: JSON.stringify(body),
});
const d = await res.json() as { jobId?: string; error?: string };
if (!res.ok) {
diff --git a/src/app/api/eterra/refresh-all/route.ts b/src/app/api/eterra/refresh-all/route.ts
new file mode 100644
index 0000000..836af69
--- /dev/null
+++ b/src/app/api/eterra/refresh-all/route.ts
@@ -0,0 +1,153 @@
+/**
+ * POST /api/eterra/refresh-all
+ *
+ * Runs delta sync on ALL UATs in DB sequentially.
+ * UATs with >30% enrichment → magic mode (sync + enrichment).
+ * UATs with ≤30% enrichment → base mode (sync only).
+ *
+ * Returns immediately with jobId — progress via /api/eterra/progress.
+ */
+
+import { PrismaClient } from "@prisma/client";
+import {
+ setProgress,
+ clearProgress,
+ type SyncProgress,
+} from "@/modules/parcel-sync/services/progress-store";
+import { syncLayer } from "@/modules/parcel-sync/services/sync-service";
+import { enrichFeatures } from "@/modules/parcel-sync/services/enrich-service";
+import { EterraClient } from "@/modules/parcel-sync/services/eterra-client";
+import { checkEterraHealthNow } from "@/modules/parcel-sync/services/eterra-health";
+
+export const runtime = "nodejs";
+export const dynamic = "force-dynamic";
+
+const prisma = new PrismaClient();
+
+export async function POST() {
+ const username = process.env.ETERRA_USERNAME ?? "";
+ const password = process.env.ETERRA_PASSWORD ?? "";
+ if (!username || !password) {
+ return Response.json(
+ { error: "ETERRA_USERNAME / ETERRA_PASSWORD not configured" },
+ { status: 500 },
+ );
+ }
+
+ const jobId = crypto.randomUUID();
+ setProgress({
+ jobId,
+ downloaded: 0,
+ total: 100,
+ status: "running",
+ phase: "Pregătire refresh complet",
+ });
+
+ void runRefreshAll(jobId, username, password);
+
+ return Response.json({ jobId, message: "Refresh complet pornit" }, { status: 202 });
+}
+
+async function runRefreshAll(jobId: string, username: string, password: string) {
+ const push = (p: Partial) =>
+ setProgress({ jobId, downloaded: 0, total: 100, status: "running", ...p } as SyncProgress);
+
+ try {
+ // Health check
+ const health = await checkEterraHealthNow();
+ if (!health.available) {
+ setProgress({ jobId, downloaded: 0, total: 100, status: "error", phase: "eTerra indisponibil", message: health.message ?? "maintenance" });
+ setTimeout(() => clearProgress(jobId), 3_600_000);
+ return;
+ }
+
+ // Find all UATs with features + enrichment ratio
+ const uats = await prisma.$queryRawUnsafe<
+ Array<{ siruta: string; name: string | null; total: number; enriched: number }>
+ >(
+ `SELECT f.siruta, u.name, COUNT(*)::int as total,
+ COUNT(*) FILTER (WHERE f."enrichedAt" IS NOT NULL)::int as enriched
+ FROM "GisFeature" f LEFT JOIN "GisUat" u ON f.siruta = u.siruta
+ WHERE f."layerId" IN ('TERENURI_ACTIVE','CLADIRI_ACTIVE') AND f."objectId" > 0
+ GROUP BY f.siruta, u.name ORDER BY total DESC`,
+ );
+
+ if (uats.length === 0) {
+ setProgress({ jobId, downloaded: 100, total: 100, status: "done", phase: "Niciun UAT in DB" });
+ setTimeout(() => clearProgress(jobId), 3_600_000);
+ return;
+ }
+
+ const results: Array<{ siruta: string; name: string; mode: string; duration: number; note: string }> = [];
+ let errors = 0;
+
+ for (let i = 0; i < uats.length; i++) {
+ const uat = uats[i]!;
+ const uatName = uat.name ?? uat.siruta;
+ const ratio = uat.total > 0 ? uat.enriched / uat.total : 0;
+ const isMagic = ratio > 0.3;
+ const mode = isMagic ? "magic" : "base";
+ const pct = Math.round(((i) / uats.length) * 100);
+
+ push({
+ downloaded: pct,
+ total: 100,
+ phase: `[${i + 1}/${uats.length}] ${uatName} (${mode})`,
+ note: results.length > 0 ? `Ultimul: ${results[results.length - 1]!.name} — ${results[results.length - 1]!.note}` : undefined,
+ });
+
+ const uatStart = Date.now();
+ try {
+ // Sync TERENURI + CLADIRI (quick-count + VALID_FROM delta)
+ const tRes = await syncLayer(username, password, uat.siruta, "TERENURI_ACTIVE", { uatName });
+ const cRes = await syncLayer(username, password, uat.siruta, "CLADIRI_ACTIVE", { uatName });
+
+ let enrichNote = "";
+ if (isMagic) {
+ const client = await EterraClient.create(username, password, { timeoutMs: 120_000 });
+ const eRes = await enrichFeatures(client, uat.siruta);
+ enrichNote = eRes.status === "done"
+ ? ` | enrich: ${eRes.enrichedCount}/${eRes.totalFeatures ?? "?"}`
+ : ` | enrich err: ${eRes.error}`;
+ }
+
+ const dur = Math.round((Date.now() - uatStart) / 1000);
+ const parts = [
+ tRes.newFeatures > 0 || (tRes.validFromUpdated ?? 0) > 0
+ ? `T:+${tRes.newFeatures}/${tRes.validFromUpdated ?? 0}vf`
+ : "T:ok",
+ cRes.newFeatures > 0 || (cRes.validFromUpdated ?? 0) > 0
+ ? `C:+${cRes.newFeatures}/${cRes.validFromUpdated ?? 0}vf`
+ : "C:ok",
+ ];
+ const note = `${parts.join(", ")}${enrichNote} (${dur}s)`;
+ results.push({ siruta: uat.siruta, name: uatName, mode, duration: dur, note });
+ console.log(`[refresh-all] ${i + 1}/${uats.length} ${uatName}: ${note}`);
+ } catch (err) {
+ errors++;
+ const dur = Math.round((Date.now() - uatStart) / 1000);
+ const msg = err instanceof Error ? err.message : "Unknown";
+ results.push({ siruta: uat.siruta, name: uatName, mode, duration: dur, note: `ERR: ${msg}` });
+ console.error(`[refresh-all] ${uatName}: ${msg}`);
+ }
+ }
+
+ const totalDur = results.reduce((s, r) => s + r.duration, 0);
+ const summary = `${uats.length} UATs, ${errors} erori, ${totalDur}s total`;
+ setProgress({
+ jobId,
+ downloaded: 100,
+ total: 100,
+ status: "done",
+ phase: "Refresh complet finalizat",
+ message: summary,
+ note: results.map((r) => `${r.name}: ${r.note}`).join("\n"),
+ });
+ console.log(`[refresh-all] Done: ${summary}`);
+ setTimeout(() => clearProgress(jobId), 6 * 3_600_000);
+ } catch (err) {
+ const msg = err instanceof Error ? err.message : "Unknown";
+ setProgress({ jobId, downloaded: 0, total: 100, status: "error", phase: "Eroare", message: msg });
+ setTimeout(() => clearProgress(jobId), 3_600_000);
+ }
+}