b7a236c45a
- 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
159 lines
4.6 KiB
TypeScript
159 lines
4.6 KiB
TypeScript
/**
|
|
* Server-side eTerra session store (global singleton).
|
|
*
|
|
* Holds one shared session for the whole app. Multiple browser clients
|
|
* see the same connection state. The EterraClient already caches sessions
|
|
* by credential hash, so we just store which credentials are active +
|
|
* metadata (who connected, when, running jobs).
|
|
*/
|
|
|
|
import { getProgress } from "./progress-store";
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Types */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
export type EterraSession = {
|
|
/** Username used to connect */
|
|
username: string;
|
|
/** Password (kept in memory only, never sent to clients) */
|
|
password: string;
|
|
/** ISO timestamp of connection */
|
|
connectedAt: string;
|
|
/** Running job IDs — tracked so we can warn before disconnect */
|
|
activeJobs: Set<string>;
|
|
};
|
|
|
|
export type SessionStatus = {
|
|
connected: boolean;
|
|
username?: string;
|
|
connectedAt?: string;
|
|
/** How many jobs are currently running */
|
|
activeJobCount: number;
|
|
/** First running job's phase (for UI hint) */
|
|
activeJobPhase?: string;
|
|
/** eTerra platform health */
|
|
eterraAvailable?: boolean;
|
|
/** True if eTerra is detected as being in maintenance */
|
|
eterraMaintenance?: boolean;
|
|
/** Human-readable health message */
|
|
eterraHealthMessage?: string;
|
|
};
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Global store */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
const g = globalThis as { __eterraSessionStore?: EterraSession | null };
|
|
|
|
function getSession(): EterraSession | null {
|
|
return g.__eterraSessionStore ?? null;
|
|
}
|
|
|
|
function setSession(session: EterraSession | null) {
|
|
g.__eterraSessionStore = session;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Public API */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
export function createSession(username: string, password: string): void {
|
|
setSession({
|
|
username,
|
|
password,
|
|
connectedAt: new Date().toISOString(),
|
|
activeJobs: new Set(),
|
|
});
|
|
}
|
|
|
|
export function destroySession(): { destroyed: boolean; reason?: string } {
|
|
const session = getSession();
|
|
if (!session) return { destroyed: true };
|
|
|
|
// Check for running jobs
|
|
const running = getRunningJobs(session);
|
|
if (running.length > 0) {
|
|
return {
|
|
destroyed: false,
|
|
reason: `Există ${running.length} job(uri) active. Așteaptă finalizarea lor înainte de deconectare.`,
|
|
};
|
|
}
|
|
|
|
setSession(null);
|
|
return { destroyed: true };
|
|
}
|
|
|
|
export function forceDestroySession(): void {
|
|
setSession(null);
|
|
}
|
|
|
|
export function getSessionCredentials(): {
|
|
username: string;
|
|
password: string;
|
|
} | null {
|
|
const session = getSession();
|
|
if (!session) return null;
|
|
return { username: session.username, password: session.password };
|
|
}
|
|
|
|
export function registerJob(jobId: string): void {
|
|
const session = getSession();
|
|
if (session) session.activeJobs.add(jobId);
|
|
}
|
|
|
|
export function unregisterJob(jobId: string): void {
|
|
const session = getSession();
|
|
if (session) session.activeJobs.delete(jobId);
|
|
}
|
|
|
|
export function getSessionStatus(): SessionStatus {
|
|
const session = getSession();
|
|
if (!session) {
|
|
return { connected: false, activeJobCount: 0 };
|
|
}
|
|
|
|
const running = getRunningJobs(session);
|
|
|
|
// Find one running job's phase for a UI hint
|
|
let activeJobPhase: string | undefined;
|
|
for (const jid of running) {
|
|
const p = getProgress(jid);
|
|
if (p?.phase) {
|
|
activeJobPhase = p.phase;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return {
|
|
connected: true,
|
|
username: session.username,
|
|
connectedAt: session.connectedAt,
|
|
activeJobCount: running.length,
|
|
activeJobPhase,
|
|
};
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Helpers */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
function getRunningJobs(session: EterraSession): string[] {
|
|
// Guard: ensure activeJobs is iterable (Set). Containers may restart
|
|
// with stale globalThis where the Set was serialized as plain object.
|
|
if (!(session.activeJobs instanceof Set)) {
|
|
session.activeJobs = new Set();
|
|
return [];
|
|
}
|
|
const running: string[] = [];
|
|
for (const jid of session.activeJobs) {
|
|
const p = getProgress(jid);
|
|
if (p && p.status === "running") {
|
|
running.push(jid);
|
|
} else {
|
|
session.activeJobs.delete(jid);
|
|
}
|
|
}
|
|
return running;
|
|
}
|