feat(parcel-sync): eTerra health check + maintenance detection

- New eterra-health.ts service: pings eTerra periodically (3min),
  detects maintenance (503, keywords), tracks consecutive failures
- New /api/eterra/health endpoint for explicit health queries
- Session route blocks login when eTerra is in maintenance (503 response)
- GET /api/eterra/session now includes eterraAvailable/eterraMaintenance
- ConnectionPill shows amber 'Mentenanță' state with AlertTriangle icon
  instead of confusing red error when eTerra is down
- Auto-connect skips when maintenance detected, retries when back online
- 30s session poll auto-detects recovery and re-enables auto-connect
This commit is contained in:
AI Assistant
2026-03-08 10:28:30 +02:00
parent 6557cd5374
commit b7a236c45a
5 changed files with 362 additions and 27 deletions
@@ -75,6 +75,12 @@ type SessionStatus = {
connectedAt?: string;
activeJobCount: number;
activeJobPhase?: string;
/** eTerra platform health */
eterraAvailable?: boolean;
/** True when eTerra is in maintenance */
eterraMaintenance?: boolean;
/** Human-readable health message */
eterraHealthMessage?: string;
};
type ExportProgress = {
@@ -153,9 +159,11 @@ function ConnectionPill({
"hover:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
session.connected
? "border-emerald-200 bg-emerald-50/80 text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-400"
: connectionError
? "border-rose-200 bg-rose-50/80 text-rose-600 dark:border-rose-800 dark:bg-rose-950/40 dark:text-rose-400"
: "border-muted-foreground/20 bg-muted/50 text-muted-foreground",
: session.eterraMaintenance
? "border-amber-200 bg-amber-50/80 text-amber-600 dark:border-amber-800 dark:bg-amber-950/40 dark:text-amber-400"
: connectionError
? "border-rose-200 bg-rose-50/80 text-rose-600 dark:border-rose-800 dark:bg-rose-950/40 dark:text-rose-400"
: "border-muted-foreground/20 bg-muted/50 text-muted-foreground",
)}
>
{connecting ? (
@@ -165,6 +173,8 @@ function ConnectionPill({
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
</span>
) : session.eterraMaintenance ? (
<AlertTriangle className="h-3 w-3" />
) : connectionError ? (
<WifiOff className="h-3 w-3" />
) : (
@@ -175,9 +185,11 @@ function ConnectionPill({
? "Se conectează…"
: session.connected
? "eTerra"
: connectionError
? "Eroare"
: "Deconectat"}
: session.eterraMaintenance
? "Mentenanță"
: connectionError
? "Eroare"
: "Deconectat"}
</span>
</button>
</DropdownMenuTrigger>
@@ -214,25 +226,52 @@ function ConnectionPill({
)}
</div>
{/* Info when not connected */}
{!session.connected && !connectionError && (
<div className="px-3 py-3 text-xs text-muted-foreground">
<p>Conexiunea se face automat când începi scrii un UAT.</p>
<p className="mt-1 text-[11px] opacity-70">
Credențialele sunt preluate din configurarea serverului.
</p>
{/* Maintenance banner */}
{!session.connected && session.eterraMaintenance && (
<div className="px-3 py-3 text-xs border-b bg-amber-50/50 dark:bg-amber-950/20">
<div className="flex items-start gap-2">
<AlertTriangle className="h-3.5 w-3.5 text-amber-500 mt-0.5 shrink-0" />
<div>
<p className="font-medium text-amber-700 dark:text-amber-400">
eTerra este în mentenanță
</p>
<p className="mt-1 text-[11px] text-amber-600/80 dark:text-amber-400/70">
Platforma ANCPI nu este disponibilă momentan. Conectarea va fi
reactivată automat când serviciul revine online.
</p>
{session.eterraHealthMessage && (
<p className="mt-1 text-[10px] opacity-60 font-mono">
{session.eterraHealthMessage}
</p>
)}
</div>
</div>
</div>
)}
{/* Error detail */}
{!session.connected && connectionError && (
<div className="px-3 py-3 text-xs text-muted-foreground">
<p>
Conexiunea automată a eșuat. Verifică credențialele din
variabilele de mediu (ETERRA_USERNAME / ETERRA_PASSWORD).
</p>
</div>
)}
{/* Info when not connected (and not in maintenance) */}
{!session.connected &&
!connectionError &&
!session.eterraMaintenance && (
<div className="px-3 py-3 text-xs text-muted-foreground">
<p>Conexiunea se face automat când începi scrii un UAT.</p>
<p className="mt-1 text-[11px] opacity-70">
Credențialele sunt preluate din configurarea serverului.
</p>
</div>
)}
{/* Error detail (only when NOT maintenance — to avoid confusing users) */}
{!session.connected &&
connectionError &&
!session.eterraMaintenance && (
<div className="px-3 py-3 text-xs text-muted-foreground">
<p>
Conexiunea automată a eșuat. Verifică credențialele din
variabilele de mediu (ETERRA_USERNAME / ETERRA_PASSWORD).
</p>
</div>
)}
{/* Connected — active jobs info + disconnect */}
{session.connected && (
@@ -444,7 +483,17 @@ export function ParcelSyncModule() {
try {
const res = await fetch("/api/eterra/session");
const data = (await res.json()) as SessionStatus;
setSession(data);
setSession((prev) => {
// If eTerra was in maintenance but is now back online, reset auto-connect
if (
prev.eterraMaintenance &&
data.eterraAvailable &&
!data.eterraMaintenance
) {
autoConnectAttempted.current = false;
}
return data;
});
if (data.connected) setConnectionError("");
return data;
} catch {
@@ -539,6 +588,8 @@ export function ParcelSyncModule() {
const triggerAutoConnect = useCallback(async () => {
if (session.connected || connecting || autoConnectAttempted.current) return;
// Don't attempt login when eTerra is in maintenance
if (session.eterraMaintenance) return;
autoConnectAttempted.current = true;
setConnecting(true);
setConnectionError("");
@@ -551,9 +602,20 @@ export function ParcelSyncModule() {
const data = (await res.json()) as {
success?: boolean;
error?: string;
maintenance?: boolean;
};
if (data.success) {
await fetchSession();
} else if (data.maintenance) {
// eTerra is in maintenance — set flag, DON'T show as connection error
setSession((prev) => ({
...prev,
eterraMaintenance: true,
eterraAvailable: false,
eterraHealthMessage: data.error ?? "eTerra în mentenanță",
}));
// Allow retry later when maintenance ends
autoConnectAttempted.current = false;
} else {
setConnectionError(data.error ?? "Eroare conectare");
}
@@ -561,7 +623,7 @@ export function ParcelSyncModule() {
setConnectionError("Eroare rețea");
}
setConnecting(false);
}, [session.connected, connecting, fetchSession]);
}, [session.connected, session.eterraMaintenance, connecting, fetchSession]);
/* ════════════════════════════════════════════════════════════ */
/* Disconnect */