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:
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* GET /api/eterra/health — eTerra platform availability check.
|
||||||
|
*
|
||||||
|
* Returns health status without requiring authentication.
|
||||||
|
* Triggers a fresh check if cached result is stale.
|
||||||
|
*
|
||||||
|
* POST /api/eterra/health — force an immediate fresh check.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import {
|
||||||
|
getEterraHealth,
|
||||||
|
checkEterraHealthNow,
|
||||||
|
} from "@/modules/parcel-sync/services/eterra-health";
|
||||||
|
|
||||||
|
export const runtime = "nodejs";
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const health = getEterraHealth();
|
||||||
|
return NextResponse.json(health);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST() {
|
||||||
|
const health = await checkEterraHealthNow();
|
||||||
|
return NextResponse.json(health);
|
||||||
|
}
|
||||||
@@ -7,16 +7,24 @@ import {
|
|||||||
getSessionCredentials,
|
getSessionCredentials,
|
||||||
getSessionStatus,
|
getSessionStatus,
|
||||||
} from "@/modules/parcel-sync/services/session-store";
|
} from "@/modules/parcel-sync/services/session-store";
|
||||||
|
import { getEterraHealth } from "@/modules/parcel-sync/services/eterra-health";
|
||||||
|
|
||||||
export const runtime = "nodejs";
|
export const runtime = "nodejs";
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/eterra/session — returns current server-side session status.
|
* GET /api/eterra/session — returns current server-side session status
|
||||||
* Any client can call this to check if eTerra is connected.
|
* enriched with eTerra platform health info.
|
||||||
*/
|
*/
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
return NextResponse.json(getSessionStatus());
|
const status = getSessionStatus();
|
||||||
|
const health = getEterraHealth();
|
||||||
|
return NextResponse.json({
|
||||||
|
...status,
|
||||||
|
eterraAvailable: health.available,
|
||||||
|
eterraMaintenance: health.maintenance,
|
||||||
|
eterraHealthMessage: health.message,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,6 +78,19 @@ export async function POST(req: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Block login when eTerra is in maintenance
|
||||||
|
const health = getEterraHealth();
|
||||||
|
if (!health.available && health.maintenance) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error:
|
||||||
|
"eTerra este în mentenanță — conectarea este dezactivată temporar",
|
||||||
|
maintenance: true,
|
||||||
|
},
|
||||||
|
{ status: 503 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if already connected with same credentials
|
// Check if already connected with same credentials
|
||||||
const existing = getSessionCredentials();
|
const existing = getSessionCredentials();
|
||||||
if (existing && existing.username === username) {
|
if (existing && existing.username === username) {
|
||||||
|
|||||||
@@ -75,6 +75,12 @@ type SessionStatus = {
|
|||||||
connectedAt?: string;
|
connectedAt?: string;
|
||||||
activeJobCount: number;
|
activeJobCount: number;
|
||||||
activeJobPhase?: string;
|
activeJobPhase?: string;
|
||||||
|
/** eTerra platform health */
|
||||||
|
eterraAvailable?: boolean;
|
||||||
|
/** True when eTerra is in maintenance */
|
||||||
|
eterraMaintenance?: boolean;
|
||||||
|
/** Human-readable health message */
|
||||||
|
eterraHealthMessage?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ExportProgress = {
|
type ExportProgress = {
|
||||||
@@ -153,9 +159,11 @@ function ConnectionPill({
|
|||||||
"hover:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
"hover:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||||
session.connected
|
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"
|
? "border-emerald-200 bg-emerald-50/80 text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-400"
|
||||||
: connectionError
|
: session.eterraMaintenance
|
||||||
? "border-rose-200 bg-rose-50/80 text-rose-600 dark:border-rose-800 dark:bg-rose-950/40 dark:text-rose-400"
|
? "border-amber-200 bg-amber-50/80 text-amber-600 dark:border-amber-800 dark:bg-amber-950/40 dark:text-amber-400"
|
||||||
: "border-muted-foreground/20 bg-muted/50 text-muted-foreground",
|
: 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 ? (
|
{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="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 className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
|
||||||
</span>
|
</span>
|
||||||
|
) : session.eterraMaintenance ? (
|
||||||
|
<AlertTriangle className="h-3 w-3" />
|
||||||
) : connectionError ? (
|
) : connectionError ? (
|
||||||
<WifiOff className="h-3 w-3" />
|
<WifiOff className="h-3 w-3" />
|
||||||
) : (
|
) : (
|
||||||
@@ -175,9 +185,11 @@ function ConnectionPill({
|
|||||||
? "Se conectează…"
|
? "Se conectează…"
|
||||||
: session.connected
|
: session.connected
|
||||||
? "eTerra"
|
? "eTerra"
|
||||||
: connectionError
|
: session.eterraMaintenance
|
||||||
? "Eroare"
|
? "Mentenanță"
|
||||||
: "Deconectat"}
|
: connectionError
|
||||||
|
? "Eroare"
|
||||||
|
: "Deconectat"}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -214,25 +226,52 @@ function ConnectionPill({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Info when not connected */}
|
{/* Maintenance banner */}
|
||||||
{!session.connected && !connectionError && (
|
{!session.connected && session.eterraMaintenance && (
|
||||||
<div className="px-3 py-3 text-xs text-muted-foreground">
|
<div className="px-3 py-3 text-xs border-b bg-amber-50/50 dark:bg-amber-950/20">
|
||||||
<p>Conexiunea se face automat când începi să scrii un UAT.</p>
|
<div className="flex items-start gap-2">
|
||||||
<p className="mt-1 text-[11px] opacity-70">
|
<AlertTriangle className="h-3.5 w-3.5 text-amber-500 mt-0.5 shrink-0" />
|
||||||
Credențialele sunt preluate din configurarea serverului.
|
<div>
|
||||||
</p>
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Error detail */}
|
{/* Info when not connected (and not in maintenance) */}
|
||||||
{!session.connected && connectionError && (
|
{!session.connected &&
|
||||||
<div className="px-3 py-3 text-xs text-muted-foreground">
|
!connectionError &&
|
||||||
<p>
|
!session.eterraMaintenance && (
|
||||||
Conexiunea automată a eșuat. Verifică credențialele din
|
<div className="px-3 py-3 text-xs text-muted-foreground">
|
||||||
variabilele de mediu (ETERRA_USERNAME / ETERRA_PASSWORD).
|
<p>Conexiunea se face automat când începi să scrii un UAT.</p>
|
||||||
</p>
|
<p className="mt-1 text-[11px] opacity-70">
|
||||||
</div>
|
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 */}
|
{/* Connected — active jobs info + disconnect */}
|
||||||
{session.connected && (
|
{session.connected && (
|
||||||
@@ -444,7 +483,17 @@ export function ParcelSyncModule() {
|
|||||||
try {
|
try {
|
||||||
const res = await fetch("/api/eterra/session");
|
const res = await fetch("/api/eterra/session");
|
||||||
const data = (await res.json()) as SessionStatus;
|
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("");
|
if (data.connected) setConnectionError("");
|
||||||
return data;
|
return data;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -539,6 +588,8 @@ export function ParcelSyncModule() {
|
|||||||
|
|
||||||
const triggerAutoConnect = useCallback(async () => {
|
const triggerAutoConnect = useCallback(async () => {
|
||||||
if (session.connected || connecting || autoConnectAttempted.current) return;
|
if (session.connected || connecting || autoConnectAttempted.current) return;
|
||||||
|
// Don't attempt login when eTerra is in maintenance
|
||||||
|
if (session.eterraMaintenance) return;
|
||||||
autoConnectAttempted.current = true;
|
autoConnectAttempted.current = true;
|
||||||
setConnecting(true);
|
setConnecting(true);
|
||||||
setConnectionError("");
|
setConnectionError("");
|
||||||
@@ -551,9 +602,20 @@ export function ParcelSyncModule() {
|
|||||||
const data = (await res.json()) as {
|
const data = (await res.json()) as {
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
maintenance?: boolean;
|
||||||
};
|
};
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
await fetchSession();
|
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 {
|
} else {
|
||||||
setConnectionError(data.error ?? "Eroare conectare");
|
setConnectionError(data.error ?? "Eroare conectare");
|
||||||
}
|
}
|
||||||
@@ -561,7 +623,7 @@ export function ParcelSyncModule() {
|
|||||||
setConnectionError("Eroare rețea");
|
setConnectionError("Eroare rețea");
|
||||||
}
|
}
|
||||||
setConnecting(false);
|
setConnecting(false);
|
||||||
}, [session.connected, connecting, fetchSession]);
|
}, [session.connected, session.eterraMaintenance, connecting, fetchSession]);
|
||||||
|
|
||||||
/* ════════════════════════════════════════════════════════════ */
|
/* ════════════════════════════════════════════════════════════ */
|
||||||
/* Disconnect */
|
/* Disconnect */
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
/**
|
||||||
|
* eTerra Health Check — server-side singleton.
|
||||||
|
*
|
||||||
|
* Periodically pings eTerra to detect availability / maintenance.
|
||||||
|
* The result is cached for `CHECK_INTERVAL_MS` to avoid hammering the server.
|
||||||
|
*
|
||||||
|
* When eTerra is down, the session route will block login attempts and
|
||||||
|
* the UI will show a friendly "mentenanță" message instead of login errors.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Types */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
export type EterraHealthStatus = {
|
||||||
|
/** Whether eTerra responded within timeout */
|
||||||
|
available: boolean;
|
||||||
|
/** Human message for the UI */
|
||||||
|
message: string;
|
||||||
|
/** ISO timestamp of last successful check */
|
||||||
|
lastCheckedAt: string;
|
||||||
|
/** ISO timestamp of last time eTerra was actually reachable */
|
||||||
|
lastAvailableAt: string | null;
|
||||||
|
/** How many consecutive failures */
|
||||||
|
consecutiveFailures: number;
|
||||||
|
/** Whether we detected a maintenance page (vs generic timeout/error) */
|
||||||
|
maintenance: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Constants */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
const BASE_URL = "https://eterra.ancpi.ro/eterra";
|
||||||
|
|
||||||
|
/** How often to re-check (3 minutes) */
|
||||||
|
const CHECK_INTERVAL_MS = 3 * 60 * 1000;
|
||||||
|
|
||||||
|
/** Timeout for the health ping itself (10 seconds) */
|
||||||
|
const PING_TIMEOUT_MS = 10_000;
|
||||||
|
|
||||||
|
/** After this many failures, consider it definitely maintenance */
|
||||||
|
const MAINTENANCE_THRESHOLD = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keywords in eTerra HTML responses that indicate maintenance.
|
||||||
|
* Romanian maintenance pages often contain these.
|
||||||
|
*/
|
||||||
|
const MAINTENANCE_KEYWORDS = [
|
||||||
|
"mentenan",
|
||||||
|
"maintenance",
|
||||||
|
"indisponibil",
|
||||||
|
"temporar",
|
||||||
|
"lucrări",
|
||||||
|
"lucrari",
|
||||||
|
"întrerupere",
|
||||||
|
"intrerupere",
|
||||||
|
"revenim",
|
||||||
|
"service unavailable",
|
||||||
|
"503",
|
||||||
|
];
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Global singleton state */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
type HealthState = {
|
||||||
|
status: EterraHealthStatus;
|
||||||
|
lastCheckStarted: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const g = globalThis as { __eterraHealthState?: HealthState };
|
||||||
|
|
||||||
|
function getState(): HealthState {
|
||||||
|
if (!g.__eterraHealthState) {
|
||||||
|
g.__eterraHealthState = {
|
||||||
|
status: {
|
||||||
|
available: true, // Optimistic default: assume available until checked
|
||||||
|
message: "Verificare în curs…",
|
||||||
|
lastCheckedAt: new Date(0).toISOString(),
|
||||||
|
lastAvailableAt: null,
|
||||||
|
consecutiveFailures: 0,
|
||||||
|
maintenance: false,
|
||||||
|
},
|
||||||
|
lastCheckStarted: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return g.__eterraHealthState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Core check */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
async function performHealthCheck(): Promise<EterraHealthStatus> {
|
||||||
|
const state = getState();
|
||||||
|
const now = Date.now();
|
||||||
|
state.lastCheckStarted = now;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// A lightweight GET to the eTerra root — we only care about reachability.
|
||||||
|
// We follow redirects (login page redirect is fine — means eTerra is UP).
|
||||||
|
const response = await axios.get(BASE_URL, {
|
||||||
|
timeout: PING_TIMEOUT_MS,
|
||||||
|
maxRedirects: 5,
|
||||||
|
validateStatus: () => true, // Accept any HTTP status as "response received"
|
||||||
|
headers: {
|
||||||
|
"User-Agent":
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = response.status;
|
||||||
|
const body =
|
||||||
|
typeof response.data === "string" ? response.data.toLowerCase() : "";
|
||||||
|
|
||||||
|
// Check if the page content indicates maintenance
|
||||||
|
const isMaintenance =
|
||||||
|
status === 503 || MAINTENANCE_KEYWORDS.some((kw) => body.includes(kw));
|
||||||
|
|
||||||
|
if (isMaintenance) {
|
||||||
|
state.status = {
|
||||||
|
available: false,
|
||||||
|
message: "eTerra este în mentenanță programată",
|
||||||
|
lastCheckedAt: new Date().toISOString(),
|
||||||
|
lastAvailableAt: state.status.lastAvailableAt,
|
||||||
|
consecutiveFailures: state.status.consecutiveFailures + 1,
|
||||||
|
maintenance: true,
|
||||||
|
};
|
||||||
|
} else if (status >= 200 && status < 500) {
|
||||||
|
// 2xx/3xx/4xx = server is responsive (4xx like 401/403 = login required = UP)
|
||||||
|
state.status = {
|
||||||
|
available: true,
|
||||||
|
message: "eTerra disponibil",
|
||||||
|
lastCheckedAt: new Date().toISOString(),
|
||||||
|
lastAvailableAt: new Date().toISOString(),
|
||||||
|
consecutiveFailures: 0,
|
||||||
|
maintenance: false,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// 5xx (but not 503 maintenance) — server error
|
||||||
|
state.status = {
|
||||||
|
available: false,
|
||||||
|
message: `eTerra indisponibil (HTTP ${status})`,
|
||||||
|
lastCheckedAt: new Date().toISOString(),
|
||||||
|
lastAvailableAt: state.status.lastAvailableAt,
|
||||||
|
consecutiveFailures: state.status.consecutiveFailures + 1,
|
||||||
|
maintenance:
|
||||||
|
state.status.consecutiveFailures + 1 >= MAINTENANCE_THRESHOLD,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as { code?: string; message?: string };
|
||||||
|
const isTimeout =
|
||||||
|
err.code === "ECONNABORTED" ||
|
||||||
|
err.code === "ETIMEDOUT" ||
|
||||||
|
err.code === "ECONNREFUSED" ||
|
||||||
|
err.code === "ECONNRESET" ||
|
||||||
|
err.code === "ENOTFOUND";
|
||||||
|
|
||||||
|
const failures = state.status.consecutiveFailures + 1;
|
||||||
|
|
||||||
|
state.status = {
|
||||||
|
available: false,
|
||||||
|
message: isTimeout
|
||||||
|
? "eTerra nu răspunde (timeout)"
|
||||||
|
: `eTerra indisponibil: ${err.message ?? "eroare necunoscută"}`,
|
||||||
|
lastCheckedAt: new Date().toISOString(),
|
||||||
|
lastAvailableAt: state.status.lastAvailableAt,
|
||||||
|
consecutiveFailures: failures,
|
||||||
|
maintenance: failures >= MAINTENANCE_THRESHOLD,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Public API */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current eTerra health status.
|
||||||
|
* Triggers a fresh check if the cached one is stale.
|
||||||
|
* Returns immediately with the cached value (the check runs in background).
|
||||||
|
*/
|
||||||
|
export function getEterraHealth(): EterraHealthStatus {
|
||||||
|
const state = getState();
|
||||||
|
const now = Date.now();
|
||||||
|
const age = now - new Date(state.status.lastCheckedAt).getTime();
|
||||||
|
|
||||||
|
// If stale, trigger a background refresh (non-blocking)
|
||||||
|
if (age > CHECK_INTERVAL_MS) {
|
||||||
|
// Prevent parallel checks
|
||||||
|
if (now - state.lastCheckStarted > PING_TIMEOUT_MS + 2000) {
|
||||||
|
void performHealthCheck();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force an immediate health check (blocking).
|
||||||
|
* Used when explicitly requested or on first load.
|
||||||
|
*/
|
||||||
|
export async function checkEterraHealthNow(): Promise<EterraHealthStatus> {
|
||||||
|
return performHealthCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if eTerra is believed to be available for login.
|
||||||
|
* Quick synchronous check for use in session route guards.
|
||||||
|
*/
|
||||||
|
export function isEterraAvailable(): boolean {
|
||||||
|
return getState().status.available;
|
||||||
|
}
|
||||||
@@ -32,6 +32,12 @@ export type SessionStatus = {
|
|||||||
activeJobCount: number;
|
activeJobCount: number;
|
||||||
/** First running job's phase (for UI hint) */
|
/** First running job's phase (for UI hint) */
|
||||||
activeJobPhase?: string;
|
activeJobPhase?: string;
|
||||||
|
/** eTerra platform health */
|
||||||
|
eterraAvailable?: boolean;
|
||||||
|
/** True if eTerra is detected as being in maintenance */
|
||||||
|
eterraMaintenance?: boolean;
|
||||||
|
/** Human-readable health message */
|
||||||
|
eterraHealthMessage?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|||||||
Reference in New Issue
Block a user