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:
@@ -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"
|
||||||
|
|||||||
@@ -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:00–05: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:00–05: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 */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|||||||
Reference in New Issue
Block a user