From 1c94f97d11bd772464ddb00cabf678c5b617625c Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Fri, 10 Apr 2026 01:17:00 +0200 Subject: [PATCH] dashboard implementation --- .dockerignore | 18 +++ Dockerfile | 4 +- README.md | 29 +++++ docker-compose.yml | 23 ++++ docs/Nxtgauge-End-to-End-Testing-Blueprint.md | 2 +- package.json | 10 +- scripts/admin-3000-service.sh | 32 ++--- scripts/restart-admin-3000.sh | 24 ++-- .../admin/DashboardDesignPreview.tsx | 25 +++- src/lib/server/gateway.ts | 2 +- src/routes/admin/[...module].tsx | 12 +- .../external-dashboard-management/index.tsx | 49 +++++++- src/routes/api/admin/[...path].ts | 113 ++++++++++++++++++ 13 files changed, 298 insertions(+), 45 deletions(-) create mode 100644 .dockerignore create mode 100644 docker-compose.yml create mode 100644 src/routes/api/admin/[...path].ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2c492e6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +.git +.gitignore +node_modules +dist +.output +output +storybook-static +playwright-report +test-results +admin.log +admin.pid +.playwright-cli +.vinxi +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +.DS_Store diff --git a/Dockerfile b/Dockerfile index 2bea590..e772e02 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,8 +21,8 @@ WORKDIR /app COPY --from=builder /app/.output ./.output -ENV PORT=3000 +ENV PORT=9202 ENV HOST=0.0.0.0 -EXPOSE 3000 +EXPOSE 9102 CMD ["node", ".output/server/index.mjs"] diff --git a/README.md b/README.md index eed692d..4e807f3 100644 --- a/README.md +++ b/README.md @@ -8,3 +8,32 @@ Port admin modules one by one with strict API/permission parity. See `docs/MIGRATION_MASTER_PLAN.md`. ## This project was created with the [Solid CLI](https://github.com/solidjs-community/solid-cli) + +## Local Docker test (low RAM, no port conflict) + +This repo ships an Alpine runtime image and a `docker-compose.yml` profile mapped to port `9102`. + +Build and run: + +```bash +docker compose up --build -d +``` + +Check: + +```bash +curl -I http://localhost:9202 +``` + +Stop: + +```bash +docker compose down +``` + +Run additional isolated instances (`9103`, `9104`, ...): + +```bash +docker run -d --name nxtgauge-admin-solid-9103 -p 9103:9202 nxtgauge-admin-solid:local +docker run -d --name nxtgauge-admin-solid-9104 -p 9104:9202 nxtgauge-admin-solid:local +``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..aba82e2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +services: + admin: + build: + context: . + dockerfile: Dockerfile + image: nxtgauge-admin-solid:local + container_name: nxtgauge-admin-solid + environment: + HOST: 0.0.0.0 + PORT: 9102 + NODE_OPTIONS: --max-old-space-size=256 + GATEWAY_URL: http://host.docker.internal:9100 + ports: + - "9202:9102" + restart: unless-stopped + mem_limit: 512m + cpus: 0.75 + healthcheck: + test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:9102/"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 20s diff --git a/docs/Nxtgauge-End-to-End-Testing-Blueprint.md b/docs/Nxtgauge-End-to-End-Testing-Blueprint.md index 7dbb18d..77d10e7 100644 --- a/docs/Nxtgauge-End-to-End-Testing-Blueprint.md +++ b/docs/Nxtgauge-End-to-End-Testing-Blueprint.md @@ -24,7 +24,7 @@ - Frontend admin app: `/Users/ashwin/workspace/nxtgauge-admin-solid` - Rust backend workspace: `/Users/ashwin/workspace/nxtgauge-backend-rust` - Required ports: - - Admin app: `http://localhost:3000` + - Admin app: `http://localhost:9202` - Storybook: `http://localhost:6006` - Preconditions: - `npm install` completed in admin repo. diff --git a/package.json b/package.json index 298c903..551c054 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,11 @@ "dev": "vinxi dev", "build": "vinxi build", "start": "vinxi start", - "start:3000": "HOST=0.0.0.0 PORT=3000 node .output/server/index.mjs", - "admin:restart:3000": "bash ./scripts/admin-3000-service.sh restart", - "admin:stop:3000": "bash ./scripts/admin-3000-service.sh stop", - "admin:status:3000": "bash ./scripts/admin-3000-service.sh status", - "admin:start:3000": "bash ./scripts/admin-3000-service.sh start", + "start:9202": "HOST=0.0.0.0 PORT=9202 node .output/server/index.mjs", + "admin:restart:9202": "bash ./scripts/admin-3000-service.sh restart", + "admin:stop:9202": "bash ./scripts/admin-3000-service.sh stop", + "admin:status:9202": "bash ./scripts/admin-3000-service.sh status", + "admin:start:9202": "bash ./scripts/admin-3000-service.sh start", "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", diff --git a/scripts/admin-3000-service.sh b/scripts/admin-3000-service.sh index 910304c..3ead502 100755 --- a/scripts/admin-3000-service.sh +++ b/scripts/admin-3000-service.sh @@ -2,12 +2,12 @@ set -euo pipefail ROOT_DIR="/Users/ashwin/workspace/nxtgauge-admin-solid" -PID_FILE="/tmp/nxtgauge-admin-3000.pid" -APP_LOG="/tmp/nxtgauge-admin-3000.log" -APP_URL="http://127.0.0.1:3000/admin/external-dashboard-management" +PID_FILE="/tmp/nxtgauge-admin-9202.pid" +APP_LOG="/tmp/nxtgauge-admin-9202.log" +APP_URL="http://127.0.0.1:9202/admin/external-dashboard-management" kill_listeners() { - lsof -tiTCP:3000 -sTCP:LISTEN | xargs -r kill -9 || true + lsof -tiTCP:9202 -sTCP:LISTEN | xargs -r kill -9 || true } start() { @@ -15,7 +15,7 @@ start() { old_pid="$(cat "$PID_FILE" || true)" if [[ -n "${old_pid:-}" ]] && kill -0 "$old_pid" 2>/dev/null; then if curl -fsS -m 3 "$APP_URL" >/dev/null 2>&1; then - echo "admin-3000 already running (pid $old_pid)" + echo "admin-9202 already running (pid $old_pid)" echo "url: $APP_URL" return 0 fi @@ -24,25 +24,25 @@ start() { rm -f "$PID_FILE" fi - pkill -f "admin-3000-daemon.sh" || true + pkill -f "admin-9202-daemon.sh" || true pkill -f ".output/server/index.mjs" || true kill_listeners - nohup /bin/zsh -lc "cd '$ROOT_DIR' && HOST=0.0.0.0 PORT=3000 node .output/server/index.mjs" >>"$APP_LOG" 2>&1 & + nohup /bin/zsh -lc "cd '$ROOT_DIR' && HOST=0.0.0.0 PORT=9202 node .output/server/index.mjs" >>"$APP_LOG" 2>&1 & new_pid=$! echo "$new_pid" > "$PID_FILE" for _ in {1..30}; do if curl -fsS -m 3 "$APP_URL" >/dev/null 2>&1; then - echo "admin-3000: running (pid $new_pid)" + echo "admin-9202: running (pid $new_pid)" echo "url: $APP_URL" return 0 fi sleep 1 done - echo "admin-3000 failed to become healthy" - tail -n 80 /tmp/nxtgauge-admin-3000.log || true + echo "admin-9202 failed to become healthy" + tail -n 80 /tmp/nxtgauge-admin-9202.log || true exit 1 } @@ -55,9 +55,9 @@ stop() { rm -f "$PID_FILE" fi - pkill -f "admin-3000-daemon.sh|.output/server/index.mjs" || true + pkill -f "admin-9202-daemon.sh|.output/server/index.mjs" || true kill_listeners - echo "admin-3000: stopped" + echo "admin-9202: stopped" } status() { @@ -70,14 +70,14 @@ status() { fi fi - if lsof -nP -iTCP:3000 -sTCP:LISTEN >/dev/null 2>&1; then - echo "admin-3000: running" + if lsof -nP -iTCP:9202 -sTCP:LISTEN >/dev/null 2>&1; then + echo "admin-9202: running" echo "process: $app_state${app_pid:+ (pid $app_pid)}" - lsof -nP -iTCP:3000 -sTCP:LISTEN + lsof -nP -iTCP:9202 -sTCP:LISTEN exit 0 fi - echo "admin-3000: stopped" + echo "admin-9202: stopped" echo "process: $app_state${app_pid:+ (pid $app_pid)}" exit 1 } diff --git a/scripts/restart-admin-3000.sh b/scripts/restart-admin-3000.sh index cc139ec..bdc251e 100755 --- a/scripts/restart-admin-3000.sh +++ b/scripts/restart-admin-3000.sh @@ -2,9 +2,9 @@ set -euo pipefail ROOT_DIR="/Users/ashwin/workspace/nxtgauge-admin-solid" -PID_FILE="/tmp/nxtgauge-admin-3000.pid" -LOG_FILE="/tmp/nxtgauge-admin-3000.log" -APP_URL="http://127.0.0.1:3000/admin/external-dashboard-management" +PID_FILE="/tmp/nxtgauge-admin-9202.pid" +LOG_FILE="/tmp/nxtgauge-admin-9202.log" +APP_URL="http://127.0.0.1:9202/admin/external-dashboard-management" stop_server() { if [[ -f "$PID_FILE" ]]; then @@ -15,16 +15,16 @@ stop_server() { rm -f "$PID_FILE" fi - lsof -tiTCP:3000 -sTCP:LISTEN | xargs -r kill -9 || true + lsof -tiTCP:9202 -sTCP:LISTEN | xargs -r kill -9 || true } status_server() { - if lsof -nP -iTCP:3000 -sTCP:LISTEN >/dev/null 2>&1; then - echo "admin-3000: running" - lsof -nP -iTCP:3000 -sTCP:LISTEN + if lsof -nP -iTCP:9202 -sTCP:LISTEN >/dev/null 2>&1; then + echo "admin-9202: running" + lsof -nP -iTCP:9202 -sTCP:LISTEN exit 0 fi - echo "admin-3000: stopped" + echo "admin-9202: stopped" exit 1 } @@ -36,20 +36,20 @@ start_server() { npm run build fi - nohup env HOST=127.0.0.1 PORT=3000 node .output/server/index.mjs >"$LOG_FILE" 2>&1 & + nohup env HOST=127.0.0.1 PORT=9202 node .output/server/index.mjs >"$LOG_FILE" 2>&1 & new_pid=$! echo "$new_pid" > "$PID_FILE" for _ in {1..20}; do if curl -fsS -m 3 "$APP_URL" >/dev/null 2>&1; then - echo "admin-3000: running (pid $new_pid)" + echo "admin-9202: running (pid $new_pid)" echo "url: $APP_URL" exit 0 fi sleep 1 done - echo "admin-3000: failed to start" + echo "admin-9202: failed to start" echo "Last log lines:" tail -n 40 "$LOG_FILE" || true exit 1 @@ -63,7 +63,7 @@ case "$action" in ;; stop) stop_server - echo "admin-3000: stopped" + echo "admin-9202: stopped" ;; status) status_server diff --git a/src/components/admin/DashboardDesignPreview.tsx b/src/components/admin/DashboardDesignPreview.tsx index cb3f494..5c9f8de 100644 --- a/src/components/admin/DashboardDesignPreview.tsx +++ b/src/components/admin/DashboardDesignPreview.tsx @@ -497,6 +497,27 @@ const PORTFOLIO_SPECS: Record = { { name: 'Royal', price: '₹1,200/plate', items: ['Custom menu', 'Chef station', 'Premium décor', 'Full-day service'] }, ], }, + JOB_SEEKER: { + roleLabel: 'Job Seeker', + tabs: ['about', 'education', 'work experience', 'skills & certifications', 'projects', 'faqs'], + specialties: ['Frontend Development', 'Backend APIs', 'QA Testing', 'Data Analysis', 'UI Design', 'Customer Support'], + statsLabels: ['Applications Sent', 'Years Exp', 'Profiles Viewed', 'Interviews', 'Open To Work'], + equipmentLabel: 'Resume Assets', + galleryTabLabel: 'projects', + experienceTabLabel: 'work experience', + serviceTabLabel: 'skills & certifications', + faqItems: [ + { q: 'What kind of roles are you targeting?', a: 'I am targeting full-time and contract roles aligned with my core skills and prior project experience.' }, + { q: 'Are you open to relocation or remote?', a: 'Yes. I am open to remote roles and relocation for the right opportunity.' }, + { q: 'How soon can you join?', a: 'I can join immediately or within the standard notice period discussed during hiring.' }, + { q: 'Can you share sample projects?', a: 'Yes. Portfolio projects and case studies are available in the Projects tab.' }, + ], + packages: [ + { name: 'Career Snapshot', price: 'Profile Section', items: ['Summary headline', 'Preferred role', 'Current location', 'Availability'] }, + { name: 'Experience Overview', price: 'Profile Section', items: ['Work history', 'Key achievements', 'Project highlights', 'Tools used'] }, + { name: 'Showcase Pack', price: 'Profile Section', items: ['Project links', 'Skill matrix', 'Certifications', 'Resume download'] }, + ], + }, PROFESSIONAL: { roleLabel: 'Professional', tabs: ['about', 'services & pricing', 'portfolio', 'experience', 'testimonials', 'faqs'], @@ -565,7 +586,7 @@ const PORTFOLIO_TESTIMONIALS = [ function customerViewFor(sidebar: string, roleKey: string): CustomerView { const key = String(sidebar || '').toLowerCase().trim(); const role = normalizeRoleKey(roleKey); - const isProfessionalRole = role !== 'COMPANY' && role !== 'JOB_SEEKER' && role !== 'CUSTOMER'; + const isProfessionalRole = role !== 'COMPANY' && role !== 'CUSTOMER'; if (key === 'my dashboard') return { title: 'Service Seeker Dashboard Overview', subtitle: 'Manage your requirements and track professional responses in real-time.', tabs: ['overview', 'recent requirements', 'quick actions'], cta: 'Post New Requirement' }; if (key === 'leads') return { title: 'Leads', subtitle: 'Browse marketplace requirements and request contact access for current opportunities.', tabs: [], cta: 'Buy Credits' }; if (key === 'jobs') { @@ -1031,7 +1052,7 @@ export default function DashboardDesignPreview(props: { }) { const isProfessionalRoleKey = (roleKey: string) => { const role = normalizeRoleKey(roleKey); - return role !== 'COMPANY' && role !== 'JOB_SEEKER' && role !== 'CUSTOMER'; + return role !== 'COMPANY' && role !== 'CUSTOMER'; }; const normalizeTabKey = (value: string) => String(value || '').trim().toLowerCase(); const isCustomerExternalMode = createMemo(() => props.mode === 'customer_external'); diff --git a/src/lib/server/gateway.ts b/src/lib/server/gateway.ts index e38ad38..9b0f6bc 100644 --- a/src/lib/server/gateway.ts +++ b/src/lib/server/gateway.ts @@ -1,5 +1,5 @@ // Server-side helper: all backend calls go through the Rust gateway -const GATEWAY_URL = (process.env.GATEWAY_URL || 'http://localhost:8000').replace(/\/+$/, ''); +const GATEWAY_URL = (process.env.GATEWAY_URL || 'http://localhost:9100').replace(/\/+$/, ''); export function gatewayUrl(path: string): string { const normalized = path.startsWith('/') ? path : `/${path}`; diff --git a/src/routes/admin/[...module].tsx b/src/routes/admin/[...module].tsx index df961fb..108a926 100644 --- a/src/routes/admin/[...module].tsx +++ b/src/routes/admin/[...module].tsx @@ -3,6 +3,8 @@ import { createMemo } from 'solid-js'; import ApprovalManagementPage from './approval'; import VerificationManagementPage from './verification'; import UsersManagementPage from './users'; +import ExternalDashboardManagementPage from './external-dashboard-management'; +import InternalDashboardManagementPage from './internal-dashboard-management'; function toTitle(value: string): string { return value @@ -12,7 +14,7 @@ function toTitle(value: string): string { .join(' '); } -const LEGACY_ADMIN_ORIGIN = import.meta.env.VITE_LEGACY_ADMIN_ORIGIN || 'http://localhost:3001'; +const LEGACY_ADMIN_ORIGIN = import.meta.env.VITE_LEGACY_ADMIN_ORIGIN || 'http://localhost:9201'; function resolveLegacyPath(modulePath: string): string { switch (modulePath) { @@ -50,6 +52,14 @@ export default function LegacyModuleShellPage() { return ; } + if (modulePath === 'external-dashboard-management' || modulePath === 'onboarding-management') { + return ; + } + + if (modulePath === 'internal-dashboard-management') { + return ; + } + const moduleName = createMemo(() => toTitle(modulePath || 'Management')); const legacyPath = createMemo(() => resolveLegacyPath(modulePath)); const legacyUrl = createMemo(() => `${LEGACY_ADMIN_ORIGIN}${legacyPath()}`); diff --git a/src/routes/admin/external-dashboard-management/index.tsx b/src/routes/admin/external-dashboard-management/index.tsx index feb0e12..c435dc3 100644 --- a/src/routes/admin/external-dashboard-management/index.tsx +++ b/src/routes/admin/external-dashboard-management/index.tsx @@ -54,6 +54,7 @@ const ROLE_BASED_SIDEBAR: Record<'PROFESSIONAL' | 'COMPANY' | 'JOB_SEEKER' | 'CU JOB_SEEKER: [ 'My Dashboard', 'My Profile', + 'My Portfolio', 'Jobs', 'My Applications', 'Saved Jobs', @@ -112,7 +113,7 @@ function hasSidebarItem(items: string[], value: string): boolean { function applyPortfolioRule(items: string[], persona: 'PROFESSIONAL' | 'COMPANY' | 'JOB_SEEKER' | 'CUSTOMER' | null): string[] { if (!items.length) return items; - if (persona === 'PROFESSIONAL') { + if (persona === 'PROFESSIONAL' || persona === 'JOB_SEEKER') { if (hasSidebarItem(items, PORTFOLIO_SIDEBAR_ITEM)) return items; const insertAfter = items.findIndex((item) => normalizeToken(item) === 'my profile'); if (insertAfter >= 0) { @@ -392,8 +393,19 @@ export default function ExternalDashboardManagementPage() { if (id) roleKeyById.set(id, key); }); - setRows(dashRows - .filter((item: any) => String(item?.audience || '').toUpperCase() === 'EXTERNAL') + const externalRoleKeySet = new Set( + roleRows + .filter((r: any) => String(r?.audience || '').toUpperCase() === 'EXTERNAL') + .map((r: any) => String(r?.key || '').toUpperCase()) + .filter(Boolean), + ); + const externalDashRows = dashRows + .filter((item: any) => { + const audience = String(item?.audience || '').toUpperCase(); + if (audience === 'EXTERNAL') return true; + const rowRoleKey = String(item?.role_key || item?.config_json?.role_key || '').toUpperCase(); + return rowRoleKey ? externalRoleKeySet.has(rowRoleKey) : false; + }) .map(normalizeDashboard) .map((row: ExternalDashboard) => { const resolvedRoleKey = String(row.roleKey || roleKeyById.get(row.roleId) || '').toUpperCase(); @@ -403,8 +415,35 @@ export default function ExternalDashboardManagementPage() { roleKey: resolvedRoleKey || row.roleKey, sidebarItems: mergeSidebarForPersona(row.sidebarItems, persona), }; - }) - .sort((a: ExternalDashboard, b: ExternalDashboard) => b.updatedAt.localeCompare(a.updatedAt))); + }); + + // Fallback: if runtime rows are empty but external roles exist, synthesize draft rows so UI isn't blank. + const fallbackRows = externalDashRows.length + ? externalDashRows + : roleRows + .filter((r: any) => String(r?.audience || '').toUpperCase() === 'EXTERNAL') + .map((r: any) => { + const roleKey = String(r?.key || '').toUpperCase(); + const roleId = String(r?.id || ''); + const roleName = String(r?.name || normalizeRoleNameFromKey(roleKey)); + const persona = personaFromKey(roleKey); + return { + id: `draft-${roleId || roleKey}`, + roleId, + roleKey, + name: `${roleName} Dashboard`, + code: `EXTERNAL-${roleKey}`, + widgets: [], + tabs: [], + sidebarItems: mergeSidebarForPersona([], persona), + fields: [], + previewPath: rolePreviewPath(roleKey), + status: 'DRAFT' as const, + updatedAt: '', + }; + }); + + setRows(fallbackRows.sort((a: ExternalDashboard, b: ExternalDashboard) => b.updatedAt.localeCompare(a.updatedAt))); } catch (e: any) { setRows([]); setRoles([]); diff --git a/src/routes/api/admin/[...path].ts b/src/routes/api/admin/[...path].ts new file mode 100644 index 0000000..57db0f6 --- /dev/null +++ b/src/routes/api/admin/[...path].ts @@ -0,0 +1,113 @@ +import { gatewayUrl, withAuthHeaders } from '~/lib/server/gateway'; + +/** + * Admin API proxy endpoint. + * Allows frontend code to call `/api/admin/*` while forwarding to Rust gateway `/api/admin/*`. + */ +export async function GET({ request, params }: { request: Request; params: any }) { + return proxyRequest('GET', request, params); +} + +export async function POST({ request, params }: { request: Request; params: any }) { + return proxyRequest('POST', request, params); +} + +export async function PUT({ request, params }: { request: Request; params: any }) { + return proxyRequest('PUT', request, params); +} + +export async function DELETE({ request, params }: { request: Request; params: any }) { + return proxyRequest('DELETE', request, params); +} + +export async function PATCH({ request, params }: { request: Request; params: any }) { + return proxyRequest('PATCH', request, params); +} + +const RETRYABLE_STATUSES = new Set([500, 502, 503, 504]); + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function fetchWithRetry( + url: string, + init: RequestInit, + options: { retries?: number; retryDelayMs?: number } = {}, +): Promise { + const retries = Math.max(0, options.retries ?? 2); + const retryDelayMs = Math.max(0, options.retryDelayMs ?? 250); + + let lastResponse: Response | null = null; + let lastError: unknown = null; + + for (let attempt = 0; attempt <= retries; attempt += 1) { + try { + const response = await fetch(url, init); + if (!RETRYABLE_STATUSES.has(response.status) || attempt === retries) { + return response; + } + lastResponse = response; + } catch (error) { + lastError = error; + if (attempt === retries) { + throw error; + } + } + + await sleep(retryDelayMs * (attempt + 1)); + } + + if (lastResponse) return lastResponse; + if (lastError) throw lastError; + throw new Error('Gateway request failed'); +} + +async function proxyRequest(method: string, request: Request, params: any) { + try { + const pathParts = Array.isArray(params?.path) + ? params.path + : (params?.path ? [params.path] : []); + const rawPath = pathParts.map((p: unknown) => String(p || '').trim()).filter(Boolean).join('/'); + const path = `/api/admin${rawPath ? `/${rawPath}` : ''}`; + const url = new URL(request.url); + const queryString = url.search ? url.search : ''; + + let body: string | undefined; + if (['POST', 'PUT', 'PATCH'].includes(method)) { + body = await request.text(); + } + + const upstreamUrl = gatewayUrl(path + queryString); + const upstreamInit: RequestInit = { + method, + headers: withAuthHeaders(request, { + 'Content-Type': request.headers.get('Content-Type') || 'application/json', + }), + body, + cache: 'no-store', + }; + + const response = (method === 'GET') + ? await fetchWithRetry(upstreamUrl, upstreamInit, { retries: 2, retryDelayMs: 250 }) + : await fetch(upstreamUrl, upstreamInit); + const responseHeaders = new Headers(); + response.headers.forEach((value, key) => { + if (!['server', 'transfer-encoding', 'connection'].includes(key.toLowerCase())) { + responseHeaders.set(key, value); + } + }); + + const responseBody = await response.text(); + return new Response(responseBody, { + status: response.status, + statusText: response.statusText, + headers: responseHeaders, + }); + } catch (error: any) { + return new Response( + JSON.stringify({ success: false, error: error?.message || 'Upstream gateway unavailable' }), + { status: 502, headers: { 'Content-Type': 'application/json' } }, + ); + } +}