Files
ArchiTools/src/modules/parcel-sync/services/session-store.ts
T
AI Assistant b7a236c45a 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
2026-03-08 10:28:30 +02:00

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;
}