feat(parcel-sync): server-side eTerra session + auto-connect on UAT typing

- Add session-store.ts: global singleton for shared eTerra session state
  with job tracking (registerJob/unregisterJob/getRunningJobs)
- Add GET/POST /api/eterra/session: connect/disconnect with job-running guard
- Export routes: credential fallback chain (body > session > env vars),
  register/unregister active jobs for disconnect protection
- Login route: also creates server-side session
- ConnectionPill: session-aware display with job count, no credential form
- Auto-connect: triggers on first UAT keystroke via autoConnectAttempted ref
- Session polling: all clients poll GET /api/eterra/session every 30s
- Multi-client: any browser sees shared connection state
This commit is contained in:
AI Assistant
2026-03-06 19:06:39 +02:00
parent 129b62758c
commit bd90c4e30f
6 changed files with 416 additions and 116 deletions
+16 -2
View File
@@ -9,6 +9,11 @@ import {
clearProgress,
setProgress,
} from "@/modules/parcel-sync/services/progress-store";
import {
getSessionCredentials,
registerJob,
unregisterJob,
} from "@/modules/parcel-sync/services/session-store";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
@@ -22,8 +27,14 @@ type ExportBundleRequest = {
};
const validate = (body: ExportBundleRequest) => {
const username = String(body.username ?? "").trim();
const password = String(body.password ?? "").trim();
// Priority: request body > session store > env vars
const session = getSessionCredentials();
const username = String(
body.username || session?.username || process.env.ETERRA_USERNAME || "",
).trim();
const password = String(
body.password || session?.password || process.env.ETERRA_PASSWORD || "",
).trim();
const siruta = String(body.siruta ?? "").trim();
const jobId = body.jobId ? String(body.jobId).trim() : undefined;
const mode = body.mode === "magic" ? "magic" : "base";
@@ -158,6 +169,7 @@ export async function POST(req: Request) {
const body = (await req.json()) as ExportBundleRequest;
const validated = validate(body);
jobId = validated.jobId;
if (jobId) registerJob(jobId);
pushProgress();
const terenuriLayer = findLayerById("TERENURI_ACTIVE");
@@ -816,6 +828,7 @@ export async function POST(req: Request) {
validated.mode === "magic"
? `eterra_uat_${validated.siruta}_magic.zip`
: `eterra_uat_${validated.siruta}_terenuri_cladiri.zip`;
if (jobId) unregisterJob(jobId);
return new Response(new Uint8Array(zipBuffer), {
headers: {
"Content-Type": "application/zip",
@@ -830,6 +843,7 @@ export async function POST(req: Request) {
note = undefined;
pushProgress();
scheduleClear(jobId);
if (jobId) unregisterJob(jobId);
const lower = errMessage.toLowerCase();
const statusCode =
lower.includes("login failed") || lower.includes("session") ? 401 : 400;