diff --git a/app/api/rast/genome/route.ts b/app/api/rast/genome/route.ts new file mode 100644 index 00000000..775835b4 --- /dev/null +++ b/app/api/rast/genome/route.ts @@ -0,0 +1,88 @@ +/** + * Server-side proxy for RAST genome data. + * + * Proxies GET /api/rast/genome from the configured modelseed-api backend. + * Runs server-side so it can access PATRIC_TOKEN and avoid CORS restrictions. + */ +import { NextRequest, NextResponse } from 'next/server'; + +function buildUpstreamCandidates(params: string): string[] { + const configured = process.env.NEXT_PUBLIC_API_BASE_URL?.replace(/\/+$/, ''); + const candidates = new Set(); + if (configured) { + candidates.add(`${configured}/api/rast/genome?${params}`); + } + candidates.add(`https://staging.modelseed.org/PMS/api/rast/genome?${params}`); + candidates.add(`https://modelseed.org/PMS/api/rast/genome?${params}`); + return Array.from(candidates); +} + +export async function GET(request: NextRequest): Promise { + const token = + request.headers.get('authorization') || + process.env.PATRIC_TOKEN; + + if (!token) { + return NextResponse.json( + { error: 'Authentication required' }, + { status: 401 }, + ); + } + + const { searchParams } = new URL(request.url); + const genomeId = searchParams.get('genome_id'); + const jobId = searchParams.get('job_id'); + + if (!genomeId) { + return NextResponse.json( + { error: 'genome_id is required' }, + { status: 400 }, + ); + } + + const params = new URLSearchParams({ genome_id: genomeId }); + if (jobId) params.set('job_id', jobId); + + const headers: HeadersInit = { + Accept: 'application/json', + Authorization: token, + }; + + const upstreamCandidates = buildUpstreamCandidates(params.toString()); + let lastStatus = 503; + let lastDetail: string | null = null; + + for (const url of upstreamCandidates) { + try { + const res = await fetch(url, { headers, cache: 'no-store' }); + + if (res.ok) { + const data: unknown = await res.json(); + return NextResponse.json(data); + } + + lastStatus = res.status; + try { + const body = await res.json() as Record; + lastDetail = typeof body.detail === 'string' ? body.detail : null; + } catch { + // ignore JSON parse failure + } + + console.warn(`[rast/genome proxy] ${url} → HTTP ${res.status}${lastDetail ? ': ' + lastDetail : ''}`); + } catch (err) { + console.warn( + `[rast/genome proxy] ${url} unreachable:`, + err instanceof Error ? err.message : err, + ); + } + } + + return NextResponse.json( + { + error: 'RAST genome data service unavailable', + detail: lastDetail ?? 'All upstream endpoints failed', + }, + { status: lastStatus }, + ); +} diff --git a/components/build-model/RastGenomePreviewDialog.tsx b/components/build-model/RastGenomePreviewDialog.tsx index fd906d9a..cc9f2e39 100644 --- a/components/build-model/RastGenomePreviewDialog.tsx +++ b/components/build-model/RastGenomePreviewDialog.tsx @@ -32,6 +32,7 @@ export default function RastGenomePreviewDialog({ open, job, onProceed, onClose if (!open || !job) return; const genomeId = job.genome_id || job.id; + const jobId = job.id; if (!genomeId) return; let cancelled = false; @@ -40,7 +41,7 @@ export default function RastGenomePreviewDialog({ open, job, onProceed, onClose setGenomeData(null); setError(null); - getRastGenomeData(genomeId) + getRastGenomeData(genomeId, jobId) .then((data) => { if (cancelled) return; setGenomeData(data); diff --git a/lib/api/modelseed.ts b/lib/api/modelseed.ts index 30444fdc..bffb1740 100644 --- a/lib/api/modelseed.ts +++ b/lib/api/modelseed.ts @@ -65,13 +65,36 @@ export interface RastGenomeJob { /** * Fetch genome annotation data from RAST. * - * Calls MSSeedSupportServer.get_rast_genome_data over JSON-RPC. - * Returns genome metadata including taxonomy, domain, features, and contigs. + * Tries José's modelseed-api endpoint first (GET /api/rast/genome), + * then falls back to MSSS JSON-RPC (MSSeedSupportServer.getRastGenomeData). * * @param genomeId - RAST genome ID to fetch data for + * @param jobId - Optional RAST job ID (needed for the modelseed-api endpoint) * @returns Promise resolving to genome data record */ -export async function getRastGenomeData(genomeId: string): Promise> { +export async function getRastGenomeData(genomeId: string, jobId?: string): Promise> { + // Try José's modelseed-api endpoint first + if (jobId) { + try { + const token = getStoredAuthUsername(); + const headers: Record = { Accept: 'application/json' }; + if (token) { + headers['Authorization'] = token; + } + const params = new URLSearchParams({ genome_id: genomeId, job_id: jobId }); + const res = await fetch(`/api/rast/genome?${params}`, { headers }); + if (res.ok) { + const data: unknown = await res.json(); + if (data && typeof data === 'object') { + return data as Record; + } + } + } catch { + // Fall through to MSSS + } + } + + // Fallback: MSSS JSON-RPC const response = await fetch(MODELSEED_SUPPORT_URL, { method: 'POST', headers: withRawTokenAuth( @@ -83,9 +106,9 @@ export async function getRastGenomeData(genomeId: string): Promise