feat(monitor): add Refresh ALL UATs button with delta sync
New endpoint POST /api/eterra/refresh-all processes all 43 UATs sequentially. UATs with >30% enrichment get magic mode, others get base sync only. Each UAT uses the new delta engine (quick-count + VALID_FROM + rolling doc check). Progress tracked via progress store. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -235,6 +235,19 @@ export default function MonitorPage() {
|
|||||||
loading={actionLoading === "warm-cache"}
|
loading={actionLoading === "warm-cache"}
|
||||||
onClick={triggerWarmCache}
|
onClick={triggerWarmCache}
|
||||||
/>
|
/>
|
||||||
|
<SyncTestButton
|
||||||
|
label="Refresh ALL UATs"
|
||||||
|
description="Delta sync pe toate cele 43 UATs (magic unde e cazul)"
|
||||||
|
siruta=""
|
||||||
|
mode="base"
|
||||||
|
includeNoGeometry={false}
|
||||||
|
actionKey="refresh-all"
|
||||||
|
actionLoading={actionLoading}
|
||||||
|
setActionLoading={setActionLoading}
|
||||||
|
addLog={addLog}
|
||||||
|
pollRef={pollRef}
|
||||||
|
customEndpoint="/api/eterra/refresh-all"
|
||||||
|
/>
|
||||||
<SyncTestButton
|
<SyncTestButton
|
||||||
label="Test Delta — Cluj-Napoca (baza)"
|
label="Test Delta — Cluj-Napoca (baza)"
|
||||||
description="Doar sync parcele+cladiri existente, fara magic (54975)"
|
description="Doar sync parcele+cladiri existente, fara magic (54975)"
|
||||||
@@ -353,12 +366,13 @@ function ActionButton({ label, description, loading, onClick }: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SyncTestButton({ label, description, siruta, mode, includeNoGeometry, actionKey, actionLoading, setActionLoading, addLog, pollRef }: {
|
function SyncTestButton({ label, description, siruta, mode, includeNoGeometry, actionKey, actionLoading, setActionLoading, addLog, pollRef, customEndpoint }: {
|
||||||
label: string; description: string; siruta: string; mode: "base" | "magic";
|
label: string; description: string; siruta: string; mode: "base" | "magic";
|
||||||
includeNoGeometry: boolean; actionKey: string; actionLoading: string;
|
includeNoGeometry: boolean; actionKey: string; actionLoading: string;
|
||||||
setActionLoading: (v: string) => void;
|
setActionLoading: (v: string) => void;
|
||||||
addLog: (type: "info" | "ok" | "error" | "wait", msg: string) => void;
|
addLog: (type: "info" | "ok" | "error" | "wait", msg: string) => void;
|
||||||
pollRef: React.MutableRefObject<ReturnType<typeof setInterval> | null>;
|
pollRef: React.MutableRefObject<ReturnType<typeof setInterval> | null>;
|
||||||
|
customEndpoint?: string;
|
||||||
}) {
|
}) {
|
||||||
const startTimeRef = useRef<number>(0);
|
const startTimeRef = useRef<number>(0);
|
||||||
const formatElapsed = () => {
|
const formatElapsed = () => {
|
||||||
@@ -372,12 +386,14 @@ function SyncTestButton({ label, description, siruta, mode, includeNoGeometry, a
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setActionLoading(actionKey);
|
setActionLoading(actionKey);
|
||||||
startTimeRef.current = Date.now();
|
startTimeRef.current = Date.now();
|
||||||
addLog("info", `[${label}] Pornire sync (${mode}, noGeom=${includeNoGeometry})...`);
|
addLog("info", `[${label}] Pornire...`);
|
||||||
try {
|
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",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
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 };
|
const d = await res.json() as { jobId?: string; error?: string };
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|||||||
@@ -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<SyncProgress>) =>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user