feat(wds): live status banner, auto-poll, and instant error emails

- Auto-poll every 15s when sync is running, 60s when idle
- Live status banner: running (with city/step), error list, weekend window waiting, connection error
- Highlight active city card and currently-running step with pulse animation
- Send immediate error email per failed step (not just at session end)
- Expose syncStatus/currentActivity/inWeekendWindow in API response
- Stop silently swallowing fetch/action errors — show them in the UI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-30 01:41:55 +03:00
parent 82a225de67
commit 4410e968db
3 changed files with 240 additions and 23 deletions
@@ -22,6 +22,22 @@ import { sendEmail } from "@/core/notifications/email-service";
const prisma = new PrismaClient();
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
/* ------------------------------------------------------------------ */
/* Live activity tracking (globalThis — same process) */
/* ------------------------------------------------------------------ */
const g = globalThis as {
__weekendSyncActivity?: {
city: string;
step: string;
startedAt: string;
} | null;
};
export function getWeekendSyncActivity() {
return g.__weekendSyncActivity ?? null;
}
/* ------------------------------------------------------------------ */
/* City queue configuration */
/* ------------------------------------------------------------------ */
@@ -315,6 +331,7 @@ export async function runWeekendDeepSync(): Promise<void> {
// Check time window
if (!stillInWindow()) {
console.log("[weekend-sync] Fereastra s-a inchis, opresc.");
g.__weekendSyncActivity = null;
await saveState(state);
await sendStatusEmail(state, log, sessionStart);
return;
@@ -323,6 +340,7 @@ export async function runWeekendDeepSync(): Promise<void> {
// Check eTerra health
if (!isEterraAvailable()) {
console.log("[weekend-sync] eTerra indisponibil, opresc.");
g.__weekendSyncActivity = null;
await saveState(state);
await sendStatusEmail(state, log, sessionStart);
return;
@@ -339,11 +357,19 @@ export async function runWeekendDeepSync(): Promise<void> {
// Execute step — fresh client per step (sessions expire after ~10 min)
console.log(`[weekend-sync] ${city.name}: ${stepName}...`);
g.__weekendSyncActivity = {
city: city.name,
step: stepName,
startedAt: new Date().toISOString(),
};
try {
const client = await EterraClient.create(username, password);
const result = await executeStep(city, stepName, client);
city.steps[stepName] = result.success ? "done" : "error";
if (!result.success) city.errorMessage = result.message;
if (!result.success) {
city.errorMessage = result.message;
await sendStepErrorEmail(city, stepName, result.message);
}
city.lastActivity = new Date().toISOString();
log.push({
city: city.name,
@@ -368,7 +394,9 @@ export async function runWeekendDeepSync(): Promise<void> {
console.error(
`[weekend-sync] ${city.name}: ${stepName} EROARE: ${msg}`,
);
await sendStepErrorEmail(city, stepName, msg);
}
g.__weekendSyncActivity = null;
stepsCompleted++;
// Save state after each step (crash safety)
@@ -400,6 +428,70 @@ export async function runWeekendDeepSync(): Promise<void> {
console.log(`[weekend-sync] Sesiune finalizata. ${stepsCompleted} pasi executati.`);
}
/* ------------------------------------------------------------------ */
/* Immediate error email */
/* ------------------------------------------------------------------ */
async function sendStepErrorEmail(
city: CityState,
step: StepName,
errorMsg: string,
): Promise<void> {
const emailTo = process.env.WEEKEND_SYNC_EMAIL;
if (!emailTo) return;
try {
const now = new Date();
const timeStr = now.toLocaleString("ro-RO", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
const stepLabel: Record<StepName, string> = {
sync_terenuri: "Sync Terenuri",
sync_cladiri: "Sync Cladiri",
import_nogeom: "Import No-Geom",
enrich: "Enrichment",
};
const html = `
<div style="font-family:system-ui,sans-serif;max-width:600px;margin:0 auto">
<h2 style="color:#ef4444;margin-bottom:4px">Weekend Sync — Eroare</h2>
<p style="color:#6b7280;margin-top:0">${timeStr}</p>
<table style="border-collapse:collapse;width:100%;border:1px solid #fecaca;border-radius:6px;background:#fef2f2">
<tr>
<td style="padding:8px 12px;font-weight:600;color:#374151">Oras</td>
<td style="padding:8px 12px">${city.name} (${city.county})</td>
</tr>
<tr>
<td style="padding:8px 12px;font-weight:600;color:#374151">Pas</td>
<td style="padding:8px 12px">${stepLabel[step]}</td>
</tr>
<tr>
<td style="padding:8px 12px;font-weight:600;color:#374151">Eroare</td>
<td style="padding:8px 12px;color:#dc2626;word-break:break-word">${errorMsg}</td>
</tr>
</table>
<p style="color:#9ca3af;font-size:11px;margin-top:16px">
Generat automat de ArchiTools Weekend Sync
</p>
</div>
`;
await sendEmail({
to: emailTo,
subject: `[ArchiTools] WDS Eroare: ${city.name}${stepLabel[step]}`,
html,
});
console.log(`[weekend-sync] Email eroare trimis: ${city.name}/${step}`);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.warn(`[weekend-sync] Nu s-a putut trimite email eroare: ${msg}`);
}
}
/* ------------------------------------------------------------------ */
/* Email status report */
/* ------------------------------------------------------------------ */