feat(wds): add manual sync trigger button with force-run mode

- triggerForceSync() resets error steps, clears lastSessionDate, starts sync immediately
- Force mode uses extended night window (22:00-05:00) instead of weekend-only
- API action 'trigger' starts sync in background, returns immediately
- 'Porneste sync' button in header (hidden when already running)
- Respects __parcelSyncRunning guard to prevent concurrent runs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-30 01:59:07 +03:00
parent 4410e968db
commit 730eee6c8a
3 changed files with 98 additions and 6 deletions
+16
View File
@@ -16,6 +16,7 @@ import {
AlertTriangle, AlertTriangle,
WifiOff, WifiOff,
Activity, Activity,
Play,
} from "lucide-react"; } from "lucide-react";
import { Button } from "@/shared/components/ui/button"; import { Button } from "@/shared/components/ui/button";
import { Input } from "@/shared/components/ui/input"; import { Input } from "@/shared/components/ui/input";
@@ -251,6 +252,21 @@ export default function WeekendDeepSyncPage() {
{lastRefresh.toLocaleTimeString("ro-RO", { hour: "2-digit", minute: "2-digit", second: "2-digit" })} {lastRefresh.toLocaleTimeString("ro-RO", { hour: "2-digit", minute: "2-digit", second: "2-digit" })}
</span> </span>
)} )}
{syncStatus !== "running" && (
<Button
variant="outline"
size="sm"
className="text-indigo-600 border-indigo-300 hover:bg-indigo-50 dark:text-indigo-400 dark:border-indigo-700 dark:hover:bg-indigo-950/30"
disabled={actionLoading}
onClick={() => {
if (window.confirm("Pornesti sincronizarea manuala? Va procesa toti pasii pending."))
void doAction({ action: "trigger" });
}}
>
<Play className="h-4 w-4 mr-1" />
Porneste sync
</Button>
)}
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
+14 -1
View File
@@ -3,6 +3,7 @@ import { PrismaClient, Prisma } from "@prisma/client";
import { import {
isWeekendWindow, isWeekendWindow,
getWeekendSyncActivity, getWeekendSyncActivity,
triggerForceSync,
} from "@/modules/parcel-sync/services/weekend-deep-sync"; } from "@/modules/parcel-sync/services/weekend-deep-sync";
const prisma = new PrismaClient(); const prisma = new PrismaClient();
@@ -151,13 +152,25 @@ export async function GET() {
export async function POST(request: Request) { export async function POST(request: Request) {
// Auth handled by middleware (route is not excluded) // Auth handled by middleware (route is not excluded)
const body = (await request.json()) as { const body = (await request.json()) as {
action: "add" | "remove" | "reset" | "reset_all" | "set_priority"; action: "add" | "remove" | "reset" | "reset_all" | "set_priority" | "trigger";
siruta?: string; siruta?: string;
name?: string; name?: string;
county?: string; county?: string;
priority?: number; priority?: number;
}; };
// Trigger is handled separately — starts sync immediately
if (body.action === "trigger") {
const result = await triggerForceSync();
if (!result.started) {
return NextResponse.json(
{ error: result.reason },
{ status: 409 },
);
}
return NextResponse.json({ ok: true, message: "Sincronizare pornita" });
}
const state = await getOrCreateState(); const state = await getOrCreateState();
switch (body.action) { switch (body.action) {
@@ -32,6 +32,7 @@ const g = globalThis as {
step: string; step: string;
startedAt: string; startedAt: string;
} | null; } | null;
__parcelSyncRunning?: boolean;
}; };
export function getWeekendSyncActivity() { export function getWeekendSyncActivity() {
@@ -175,7 +176,12 @@ export function isWeekendWindow(): boolean {
} }
/** Check if still within the window (called during processing) */ /** Check if still within the window (called during processing) */
function stillInWindow(): boolean { function stillInWindow(force?: boolean): boolean {
if (force) {
// Force mode: any night 22:0005:00
const hour = new Date().getHours();
return hour >= 22 || hour < 5;
}
const hour = new Date().getHours(); const hour = new Date().getHours();
// We can be in 23,0,1,2,3 — stop at 4 // We can be in 23,0,1,2,3 — stop at 4
if (hour >= WEEKEND_END_HOUR && hour < WEEKEND_START_HOUR) return false; if (hour >= WEEKEND_END_HOUR && hour < WEEKEND_START_HOUR) return false;
@@ -273,7 +279,10 @@ type SessionLog = {
message: string; message: string;
}; };
export async function runWeekendDeepSync(): Promise<void> { export async function runWeekendDeepSync(options?: {
force?: boolean;
}): Promise<void> {
const force = options?.force ?? false;
const username = process.env.ETERRA_USERNAME; const username = process.env.ETERRA_USERNAME;
const password = process.env.ETERRA_PASSWORD; const password = process.env.ETERRA_PASSWORD;
if (!username || !password) return; if (!username || !password) return;
@@ -286,8 +295,8 @@ export async function runWeekendDeepSync(): Promise<void> {
const state = await loadState(); const state = await loadState();
const today = new Date().toISOString().slice(0, 10); const today = new Date().toISOString().slice(0, 10);
// Prevent running twice in the same session // Prevent running twice in the same session (force bypasses)
if (state.lastSessionDate === today) return; if (!force && state.lastSessionDate === today) return;
state.totalSessions++; state.totalSessions++;
state.lastSessionDate = today; state.lastSessionDate = today;
@@ -329,7 +338,7 @@ export async function runWeekendDeepSync(): Promise<void> {
for (const city of needsStep) { for (const city of needsStep) {
// Check time window // Check time window
if (!stillInWindow()) { if (!stillInWindow(force)) {
console.log("[weekend-sync] Fereastra s-a inchis, opresc."); console.log("[weekend-sync] Fereastra s-a inchis, opresc.");
g.__weekendSyncActivity = null; g.__weekendSyncActivity = null;
await saveState(state); await saveState(state);
@@ -619,6 +628,60 @@ async function sendStatusEmail(
} }
} }
/* ------------------------------------------------------------------ */
/* Manual force trigger */
/* ------------------------------------------------------------------ */
/**
* Trigger a sync run outside the weekend window.
* Resets error steps, clears lastSessionDate, and starts immediately.
* Uses an extended night window (22:0005:00) for the stillInWindow check.
*/
export async function triggerForceSync(): Promise<{ started: boolean; reason?: string }> {
if (g.__parcelSyncRunning) {
return { started: false, reason: "O sincronizare ruleaza deja" };
}
const username = process.env.ETERRA_USERNAME;
const password = process.env.ETERRA_PASSWORD;
if (!username || !password) {
return { started: false, reason: "ETERRA credentials lipsesc" };
}
if (!isEterraAvailable()) {
return { started: false, reason: "eTerra indisponibil" };
}
// Reset error steps + lastSessionDate in DB so the run proceeds
const state = await loadState();
for (const city of state.cities) {
for (const step of STEPS) {
if (city.steps[step] === "error") {
city.steps[step] = "pending";
city.errorMessage = undefined;
}
}
}
state.lastSessionDate = undefined;
await saveState(state);
// Start in background — don't block the API response
g.__parcelSyncRunning = true;
void (async () => {
try {
console.log("[weekend-sync] Force sync declansat manual.");
await runWeekendDeepSync({ force: true });
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.error(`[weekend-sync] Force sync eroare: ${msg}`);
} finally {
g.__parcelSyncRunning = false;
}
})();
return { started: true };
}
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* N8N Webhook — trigger PMTiles rebuild after sync cycle */ /* N8N Webhook — trigger PMTiles rebuild after sync cycle */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */