From 6661af46fb2627bd58c5213cac234e918747d926 Mon Sep 17 00:00:00 2001 From: VibhavSetlur Date: Wed, 13 May 2026 14:11:06 -0500 Subject: [PATCH 1/6] chore: add *.bak to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 89f41ed8..3835bdc4 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ lib/REVIEW_SUMMARY.md # misc .DS_Store *.pem +*.bak .vscode/ # debug From 54b7b83ec87aa833fae8aa7477aa4a6b744e8113 Mon Sep 17 00:00:00 2001 From: VibhavSetlur Date: Wed, 13 May 2026 14:11:11 -0500 Subject: [PATCH 2/6] fix(ui): restore missing equation columns on FBA flux and gapfill detail tables The reaction fluxes table on /fba/[...path] and the reactions table on /gapfill/[...path] were both missing the equation column, even though the data was parsed and stored in each row. Users performing manual curation could not see reaction equations (with cpd IDs) to detect duplicate-compound-ID issues that cause flux breakdowns. Added the Equation column using the ChemicalEquation component between Name and Flux/Direction, matching the pattern used on the model detail page and biochem reactions list. --- app/fba/[...path]/page.tsx | 9 +++++++++ app/gapfill/[...path]/page.tsx | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/app/fba/[...path]/page.tsx b/app/fba/[...path]/page.tsx index 57ba727f..7b18966f 100644 --- a/app/fba/[...path]/page.tsx +++ b/app/fba/[...path]/page.tsx @@ -30,6 +30,7 @@ import { getModelFbaDataFromApi, getModelFbaFromApi } from '@/lib/api/modelseed' import { useAuth } from '@/components/auth/AuthProvider'; import { workspaceGet, workspaceLs, workspaceDownloadUrl, parseWorkspaceGetObject } from '@/lib/api/workspace'; import { USE_MODELSEED_API } from '@/lib/api/config'; +import ChemicalEquation from '@/components/ui/ChemicalEquation'; import DataControlHeader from '@/components/layout/DataControlHeader'; /* ---------- types ---------- */ @@ -623,6 +624,14 @@ export default function FbaPage({ params }: { params: Promise<{ path: string[] } }, }, { field: 'name', headerName: 'Name', width: 240 }, + { + field: 'equation', + headerName: 'Equation', + flex: 1, + minWidth: 280, + sortable: false, + renderCell: (params) => , + }, { field: 'flux', headerName: 'Flux', width: 120, type: 'number' }, { field: 'min', headerName: 'Min', width: 100, type: 'number' }, { field: 'max', headerName: 'Max', width: 100, type: 'number' }, diff --git a/app/gapfill/[...path]/page.tsx b/app/gapfill/[...path]/page.tsx index 79963da5..34234dfe 100644 --- a/app/gapfill/[...path]/page.tsx +++ b/app/gapfill/[...path]/page.tsx @@ -24,6 +24,7 @@ import { DataGrid, GridColDef, GridPaginationModel, GridSortModel } from '@mui/x import { listModelGapfillsFromApi } from '@/lib/api/modelseed'; import { workspaceGet, workspaceDownloadUrl, parseWorkspaceGetObject } from '@/lib/api/workspace'; import { USE_MODELSEED_API } from '@/lib/api/config'; +import ChemicalEquation from '@/components/ui/ChemicalEquation'; import DataControlHeader from '@/components/layout/DataControlHeader'; /* ---------- types ---------- */ @@ -317,6 +318,14 @@ export default function GapfillPage({ params }: { params: Promise<{ path: string }, }, { field: 'name', headerName: 'Name', width: 280 }, + { + field: 'equation', + headerName: 'Equation', + flex: 1, + minWidth: 280, + sortable: false, + renderCell: (params) => , + }, { field: 'direction', headerName: 'Direction', width: 120 }, { field: 'compartment', headerName: 'Compartment', width: 140 }, ], []); From c83e073106b23068bbaf73a62a8e27a4eea86ab4 Mon Sep 17 00:00:00 2001 From: VibhavSetlur Date: Wed, 13 May 2026 14:11:16 -0500 Subject: [PATCH 3/6] feat(rast): add genome preview dialog with getRastGenomeData and fallback flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added getRastGenomeData() to modelseed.ts that calls MSSS over JSON-RPC to fetch genome annotation data (taxonomy, domain, features, contigs). Returns a KBase Genome-compatible dict once the backend translator is wired by José. - Wired RastGenomePreviewDialog into the RAST tab on the Build Model page. Clicking 'Build Model' on a RAST row now opens a preview dialog showing genome metadata. If genome data fetch fails (MSSS getRastGenomeData is currently broken due to chestnut MySQL), a warning is shown and the user can still proceed with default settings. - Made RastGenomePreviewDialog non-blocking on error: shows warning severity instead of error, enables 'Proceed anyway' button, so the existing submission flow is preserved regardless of backend availability. - Added API proxy route at /api/rast/jobs for server-side RAST job listing. - Added E2E tests for RAST genome listing flow. --- app/(build-model)/plant/page.tsx | 38 +++-- app/api/rast/jobs/route.ts | 83 +++++++++++ .../build-model/RastGenomePreviewDialog.tsx | 138 ++++++++++++++++++ lib/api/modelseed.ts | 55 +++++++ tests/e2e/build-model/rast-genomes.spec.ts | 128 ++++++++++++++++ 5 files changed, 430 insertions(+), 12 deletions(-) create mode 100644 app/api/rast/jobs/route.ts create mode 100644 components/build-model/RastGenomePreviewDialog.tsx create mode 100644 tests/e2e/build-model/rast-genomes.spec.ts diff --git a/app/(build-model)/plant/page.tsx b/app/(build-model)/plant/page.tsx index 9731cb00..a91b7848 100644 --- a/app/(build-model)/plant/page.tsx +++ b/app/(build-model)/plant/page.tsx @@ -22,6 +22,7 @@ import { useAuth } from '@/components/auth/AuthProvider'; import { RastGenomeJob, submitReconstructJobFromApi } from '@/lib/api/modelseed'; import { extractTrackedJobId, trackJob } from '@/lib/api/jobTracker'; import PatricGenomesTable from '@/components/build-model/PatricGenomesTable'; +import RastGenomePreviewDialog from '@/components/build-model/RastGenomePreviewDialog'; import RastGenomesTable from '@/components/build-model/RastGenomesTable'; import { PatricGenome } from '@/lib/api/patric'; @@ -102,6 +103,7 @@ export default function BuildModelPlantPage() { const [errorMessage, setErrorMessage] = useState(null); const [successMessage, setSuccessMessage] = useState(null); const [plantseedDialogOpen, setPlantseedDialogOpen] = useState(false); + const [selectedRastJob, setSelectedRastJob] = useState(null); const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => { if (PLANTSEED_MAINTENANCE && newValue === 0) { @@ -230,6 +232,11 @@ export default function BuildModelPlantPage() { }; const handleRastGenomeSelect = (job: RastGenomeJob) => { + setSelectedRastJob(job); + }; + + const handleProceedFromRastPreview = (job: RastGenomeJob) => { + setSelectedRastJob(null); const genomeId = job.genome_id || job.id; void handleReferenceSubmit('rast', 'RAST', genomeId, job.genome_name); }; @@ -441,18 +448,25 @@ export default function BuildModelPlantPage() { - setPlantseedDialogOpen(false)} - maxWidth="sm" - fullWidth - > - - - - + setSelectedRastJob(null)} + /> + + setPlantseedDialogOpen(false)} + maxWidth="sm" + fullWidth + > + + + + ); diff --git a/app/api/rast/jobs/route.ts b/app/api/rast/jobs/route.ts new file mode 100644 index 00000000..6161e19d --- /dev/null +++ b/app/api/rast/jobs/route.ts @@ -0,0 +1,83 @@ +/** + * Server-side proxy for RAST genome annotation jobs. + * + * Proxies GET /api/rast/jobs from the configured modelseed-api backend. + * Runs server-side so it can access PATRIC_TOKEN and avoid CORS restrictions. + * + * The upstream endpoint requires MODELSEED_RAST_DB_HOST to be set on the + * modelseed-api server. Returns 503 with a clear message when not configured. + */ +import { NextRequest, NextResponse } from 'next/server'; + +/** Build a deduplicated list of upstream URLs to try in priority order. */ +function buildUpstreamCandidates(): string[] { + const configured = process.env.NEXT_PUBLIC_API_BASE_URL?.replace(/\/+$/, ''); + const candidates = new Set(); + if (configured) { + candidates.add(`${configured}/api/rast/jobs`); + } + candidates.add('https://staging.modelseed.org/PMS/api/rast/jobs'); + candidates.add('https://modelseed.org/PMS/api/rast/jobs'); + return Array.from(candidates); +} + +const UPSTREAM_CANDIDATES = buildUpstreamCandidates(); + +export async function GET(request: NextRequest): Promise { + // Prefer the token from the client request; fall back to server-side env + const token = + request.headers.get('authorization') || + process.env.PATRIC_TOKEN; + + if (!token) { + return NextResponse.json( + { error: 'Authentication required' }, + { status: 401 }, + ); + } + + const headers: HeadersInit = { + Accept: 'application/json', + Authorization: token, + }; + + let lastStatus = 503; + let lastDetail: string | null = null; + + for (const url of UPSTREAM_CANDIDATES) { + try { + const res = await fetch(url, { headers, cache: 'no-store' }); + + if (res.ok) { + const data: unknown = await res.json(); + return NextResponse.json(data); + } + + // Capture the detail for the final error response + lastStatus = res.status; + try { + const body = await res.json() as Record; + lastDetail = typeof body.detail === 'string' ? body.detail : null; + } catch { + // ignore JSON parse failure on error body + } + + console.warn(`[rast/jobs proxy] ${url} → HTTP ${res.status}${lastDetail ? ': ' + lastDetail : ''}`); + } catch (err) { + console.warn( + `[rast/jobs proxy] ${url} unreachable:`, + err instanceof Error ? err.message : err, + ); + } + } + + // All upstreams failed — return the last known status/detail + return NextResponse.json( + { + error: 'RAST jobs service unavailable', + detail: lastDetail ?? 'All upstream endpoints failed', + jobs: [], + }, + { status: lastStatus }, + ); +} diff --git a/components/build-model/RastGenomePreviewDialog.tsx b/components/build-model/RastGenomePreviewDialog.tsx new file mode 100644 index 00000000..6b366fec --- /dev/null +++ b/components/build-model/RastGenomePreviewDialog.tsx @@ -0,0 +1,138 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import DialogActions from '@mui/material/DialogActions'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; +import CircularProgress from '@mui/material/CircularProgress'; +import Alert from '@mui/material/Alert'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableRow from '@mui/material/TableRow'; +import { getRastGenomeData, RastGenomeJob } from '@/lib/api/modelseed'; + +interface RastGenomePreviewDialogProps { + open: boolean; + job: RastGenomeJob | null; + onProceed: (job: RastGenomeJob) => void; + onClose: () => void; +} + +export default function RastGenomePreviewDialog({ open, job, onProceed, onClose }: RastGenomePreviewDialogProps) { + const [genomeData, setGenomeData] = useState | null>(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!open || !job) return; + + const genomeId = job.genome_id || job.id; + if (!genomeId) { + setError('No genome ID available'); + return; + } + + setLoading(true); + setGenomeData(null); + setError(null); + + getRastGenomeData(genomeId) + .then((data) => { + setGenomeData(data); + setLoading(false); + }) + .catch((err) => { + setError(err instanceof Error ? err.message : 'Failed to fetch genome data'); + setLoading(false); + }); + }, [open, job]); + + const metadataRows: { label: string; value: string }[] = []; + if (genomeData) { + const fields: [string, string][] = [ + ['id', 'Genome ID'], + ['name', 'Name'], + ['genome_id', 'NCBI ID'], + ['source', 'Source'], + ['taxonomy', 'Taxonomy'], + ['domain', 'Domain'], + ['genetic_code', 'Genetic Code'], + ['gc_content', 'GC Content'], + ['dna_size', 'DNA Size'], + ['contig_count', 'Contigs'], + ['feature_count', 'Features'], + ['pegasus', 'Pegasus'], + ]; + for (const [key, label] of fields) { + const val = genomeData[key]; + if (val != null && val !== '') { + metadataRows.push({ label, value: String(val).slice(0, 300) }); + } + } + } + + return ( + + + RAST Genome Data — {job?.genome_name || job?.genome_id || job?.id} + + + {loading && ( + + + + )} + + {error && !loading && ( + + {error} + + Genome data is unavailable, but you can still proceed to build the model with default settings. + + + )} + + {genomeData && !loading && ( + <> + + Genome data loaded + + {genomeData.features && Array.isArray(genomeData.features) && ( + + Features: {genomeData.features.length.toLocaleString()} + {genomeData.contigs && Array.isArray(genomeData.contigs) + ? ` | Contigs: ${genomeData.contigs.length.toLocaleString()}` + : ''} + + )} + + + {metadataRows.map((row) => ( + + {row.label} + {row.value} + + ))} + +
+ + )} +
+ + + + +
+ ); +} diff --git a/lib/api/modelseed.ts b/lib/api/modelseed.ts index 24437078..e236d45b 100644 --- a/lib/api/modelseed.ts +++ b/lib/api/modelseed.ts @@ -62,6 +62,61 @@ export interface RastGenomeJob { type: 'Genome'; } +/** + * Fetch genome annotation data from RAST. + * + * Calls MSSeedSupportServer.get_rast_genome_data over JSON-RPC. + * Returns genome metadata including taxonomy, domain, features, and contigs. + * + * @param genomeId - RAST genome ID to fetch data for + * @returns Promise resolving to genome data record + */ +export async function getRastGenomeData(genomeId: string): Promise> { + const response = await fetch(MODELSEED_SUPPORT_URL, { + method: 'POST', + headers: withRawTokenAuth( + { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + true, + ), + body: JSON.stringify({ + version: '1.1', + method: 'MSSeedSupportServer.get_rast_genome_data', + id: 'get-rast-genome-data', + params: [{ genome_id: genomeId }], + }), + }); + + const { payload: rawPayload, rawText } = await parseJsonResponse(response); + const payload = rawPayload as RastJobsRpcResponse | null; + + if (!response.ok) { + if (payload?.error) { + throw new Error(payload.error.message || payload.error.error || 'RAST genome data fetch failed'); + } + throw new Error( + `RAST genome data fetch failed (${response.status})${rawText ? `: ${rawText}` : ''}`, + ); + } + + if (!payload) { + throw new Error('RAST genome data returned an empty or non-JSON response'); + } + + if (payload.error) { + throw new Error(payload.error.message || payload.error.error || 'RAST genome data fetch failed'); + } + + const result = Array.isArray(payload.result) ? payload.result[0] : payload.result; + if (!result || typeof result !== 'object') { + throw new Error('Unexpected RAST genome data format'); + } + + return result as Record; +} + export interface ModelDetailBundle { ref: string; data: Record; diff --git a/tests/e2e/build-model/rast-genomes.spec.ts b/tests/e2e/build-model/rast-genomes.spec.ts new file mode 100644 index 00000000..3868a122 --- /dev/null +++ b/tests/e2e/build-model/rast-genomes.spec.ts @@ -0,0 +1,128 @@ +import { test, expect, type Page, type Route } from '@playwright/test'; + +const PATRIC_TOKEN = process.env.PATRIC_TOKEN; + +const MOCK_RAST_JOBS = [ + { owner: 'seaver', contig_count: 6188, genome_size: 6007980, mod_time: '2012-05-08 23:08:29', project: 'seaver_6666666', genome_name: 'Unknown sp.', type: 'Genome', id: '50639', creation_time: '2012-05-03 16:34:53', genome_id: '6666666.16170' }, + { owner: 'seaver', contig_count: 1, genome_size: 4641652, mod_time: '2017-09-12 14:09:39', project: 'seaver_6666666', genome_name: 'Escherichia coli str. K12 substr. MG1655', type: 'Genome', id: '502220', creation_time: '2017-09-12 14:00:24', genome_id: '6666666.279675' }, +]; + +const MSSS_LIST_RESPONSE = { version: '1.1', result: [[...MOCK_RAST_JOBS]], id: 'list-rast-genomes' }; +const MSSS_EMPTY_RESPONSE = { version: '1.1', result: [[]], id: 'list-rast-genomes' }; +const PROXY_EMPTY_RESPONSE = { result: [[]] }; + +async function authenticatePage(page: Page, token: string): Promise { + await page.addInitScript((t: string) => { + window.localStorage.setItem('auth', JSON.stringify({ user_id: 'seaver', token: t, method: 'PATRIC' })); + }, token); +} + +async function mockMsssListRoute(page: Page, response: object = MSSS_LIST_RESPONSE): Promise { + await page.route('**/services/ms_fba', async (route: Route) => { + const postData = route.request().postDataJSON(); + if (postData?.method === 'MSSeedSupportServer.list_rast_jobs') { + await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(response) }); + } else { + await route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ version: '1.1', error: { code: -32603, message: 'Mock error' }, id: postData?.id ?? 'mock' }) }); + } + }); +} + +async function mockProxyRoute(page: Page, response: object = PROXY_EMPTY_RESPONSE): Promise { + await page.route('**/api/rast/jobs', async (route: Route) => { + await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(response) }); + }); +} + +async function clickRastTab(page: Page): Promise { + await page.goto('/plant'); + await page.getByRole('tab', { name: /RAST Microbes/i }).click(); + await page.waitForTimeout(1000); +} + +test.describe('RAST Genomes Table — Build Model Page', () => { + + test.describe('Authenticated: RAST table loads and displays genome jobs', () => { + test.skip(!PATRIC_TOKEN, 'PATRIC_TOKEN not set in .env.local'); + + test.beforeEach(async ({ page }) => { + await authenticatePage(page, PATRIC_TOKEN!); + await mockMsssListRoute(page); + await mockProxyRoute(page); + }); + + test('displays RAST genome jobs in the data grid', async ({ page }) => { + await clickRastTab(page); + const grid = page.locator('.MuiDataGrid-root'); + await expect(grid).toBeVisible({ timeout: 10000 }); + await expect(grid.getByText('Escherichia coli str. K12 substr. MG1655')).toBeVisible({ timeout: 5000 }); + await expect(grid.getByText('6666666.279675')).toBeVisible({ timeout: 3000 }); + }); + + test('shows Build Model button for each row', async ({ page }) => { + await clickRastTab(page); + const buildButtons = page.locator('button:has-text("Build Model")'); + await expect(buildButtons.first()).toBeVisible({ timeout: 10000 }); + expect(await buildButtons.count()).toBeGreaterThanOrEqual(2); + }); + + test('handles empty RAST job list gracefully', async ({ page }) => { + await mockMsssListRoute(page, MSSS_EMPTY_RESPONSE); + await clickRastTab(page); + const banner = page.locator('text=RAST genome jobs are temporarily unavailable'); + await expect(banner).toBeVisible({ timeout: 10000 }); + }); + + test('handles MSSS server error gracefully', async ({ page }) => { + await page.route('**/services/ms_fba', async (route: Route) => { + await route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ version: '1.1', error: { code: -32603, message: 'Internal server error' }, id: 'list-rast-genomes' }) }); + }); + await clickRastTab(page); + const banner = page.locator('text=RAST genome jobs are temporarily unavailable'); + await expect(banner).toBeVisible({ timeout: 10000 }); + }); + + test('falls back to proxy when MSSS returns auth error', async ({ page }) => { + await page.route('**/services/ms_fba', async (route: Route) => { + await route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ version: '1.1', error: { code: -32603, message: 'Authentication required' }, id: 'list-rast-genomes' }) }); + }); + await page.route('**/api/rast/jobs', async (route: Route) => { + await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MSSS_LIST_RESPONSE) }); + }); + await clickRastTab(page); + const grid = page.locator('.MuiDataGrid-root'); + await expect(grid).toBeVisible({ timeout: 10000 }); + await expect(grid.getByText('Escherichia coli str. K12 substr. MG1655')).toBeVisible({ timeout: 5000 }); + }); + + test('opens genome preview dialog on Build Model click', async ({ page }) => { + await clickRastTab(page); + const grid = page.locator('.MuiDataGrid-root'); + await expect(grid).toBeVisible({ timeout: 10000 }); + + await page.locator('button:has-text("Build Model")').first().click(); + await page.waitForTimeout(1500); + + const dialog = page.locator('[role="dialog"]'); + await expect(dialog).toBeVisible({ timeout: 8000 }); + await expect(dialog.getByText(/RAST Genome Data/)).toBeVisible({ timeout: 5000 }); + }); + }); + + test.describe('Unauthenticated: graceful empty state', () => { + test('shows AuthGuard message when not logged in', async ({ page }) => { + await mockMsssListRoute(page); + await page.goto('/plant'); + await expect(page.getByText('Authentication Required')).toBeVisible({ timeout: 10000 }); + }); + + test('does not throw console errors when unauthenticated', async ({ page }) => { + const errors: string[] = []; + page.on('pageerror', (err) => errors.push(err.message)); + + await page.goto('/plant'); + await page.waitForTimeout(3000); + expect(errors.filter(e => !e.includes('ResizeObserver'))).toEqual([]); + }); + }); +}); From 09ee64e6b811e6250deae26f901a32218d066f7e Mon Sep 17 00:00:00 2001 From: VibhavSetlur Date: Wed, 13 May 2026 15:42:25 -0500 Subject: [PATCH 4/6] feat(rast): try modelseed-api proxy for job listing before falling back to MSSS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit listRastGenomes() now calls GET /api/rast/jobs (local proxy -> José's modelseed-api endpoint) first. If proxy returns non-200 or throws, falls back to direct MSSS JSON-RPC. Seamless switch once MODELSEED_RAST_DB_HOST is configured on the server. --- lib/api/modelseed.ts | 75 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 13 deletions(-) diff --git a/lib/api/modelseed.ts b/lib/api/modelseed.ts index e236d45b..30444fdc 100644 --- a/lib/api/modelseed.ts +++ b/lib/api/modelseed.ts @@ -536,6 +536,67 @@ type RawRastJob = { * ``` */ export async function listRastGenomes(): Promise { + // Try José's modelseed-api endpoint first via the local proxy. + // Falls back to direct MSSS JSON-RPC if the proxy is unavailable. + try { + const token = getStoredAuthUsername(); + const headers: Record = { Accept: 'application/json' }; + if (token) { + headers['Authorization'] = token; + } + const proxyRes = await fetch('/api/rast/jobs', { headers }); + if (proxyRes.ok) { + const data: unknown = await proxyRes.json(); + if (Array.isArray(data)) { + return (data as RawRastJob[]) + .filter((job) => String(job.type ?? '') === 'Genome') + .map((job) => { + const id = String(job.id ?? ''); + const genomeId = String(job.genome_id ?? ''); + return { + id, + genome_id: genomeId, + genome_name: String(job.genome_name ?? genomeId ?? id), + contig_count: + typeof job.contig_count === 'number' + ? job.contig_count + : Number.isFinite(Number(job.contig_count)) + ? Number(job.contig_count) + : undefined, + mod_time: job.mod_time ? String(job.mod_time) : undefined, + type: 'Genome', + } satisfies RastGenomeJob; + }); + } + // Some backends wrap in { jobs: [...] } + const wrapped = data as Record; + if (wrapped.jobs && Array.isArray(wrapped.jobs)) { + return (wrapped.jobs as RawRastJob[]) + .filter((job) => String(job.type ?? '') === 'Genome') + .map((job) => { + const id = String(job.id ?? ''); + const genomeId = String(job.genome_id ?? ''); + return { + id, + genome_id: genomeId, + genome_name: String(job.genome_name ?? genomeId ?? id), + contig_count: + typeof job.contig_count === 'number' + ? job.contig_count + : Number.isFinite(Number(job.contig_count)) + ? Number(job.contig_count) + : undefined, + mod_time: job.mod_time ? String(job.mod_time) : undefined, + type: 'Genome', + } satisfies RastGenomeJob; + }); + } + } + } catch { + // Proxy unavailable — fall through to MSSS + } + + // Fallback: direct MSSS JSON-RPC (legacy path) const callRastList = async (method: string, params: Record) => { const response = await fetch(MODELSEED_SUPPORT_URL, { method: 'POST', @@ -557,8 +618,6 @@ export async function listRastGenomes(): Promise { const payload = rawPayload as RastJobsRpcResponse | null; if (!response.ok) { - // Some deployments return RPC JSON error payloads with HTTP 500. - // Preserve payload so caller can apply compatibility fallbacks. if (payload?.error) { return payload; } @@ -575,10 +634,7 @@ export async function listRastGenomes(): Promise { }; const candidateMethods = [ - // Legacy ModelSEED UI used `service=msSupport` + `method=list_rast_jobs`, - // which resolved to `MSSeedSupportServer.list_rast_jobs`. 'MSSeedSupportServer.list_rast_jobs', - // Keep compatibility fallbacks for any non-standard deployments. 'msSupport.list_rast_jobs', 'ms_fba.list_rast_jobs', ]; @@ -599,13 +655,9 @@ export async function listRastGenomes(): Promise { methodErrors.push( `${method} (${paramsLabel}): ${message || `error code ${attempt.error.code ?? 'unknown'}`}`, ); - // -32601 indicates "method not found" in most deployments. Some gateways also use it - // for "package not found". Either way, move to the next compatible method name. if (attempt.error.code === -32601) { break; } - // This backend error appears when owner cannot be resolved internally. - // Try the alternate param payload before failing. if ((attempt.error.message || '').includes('selectall_arrayref')) { continue; } @@ -625,8 +677,6 @@ export async function listRastGenomes(): Promise { } if (!payload) { - // Preserve legacy behavior: if backend-side RAST listing is broken, avoid hard failure - // in the Build Model tab and return an empty list with a clear warning in the console. if (methodErrors.some((entry) => entry.includes('selectall_arrayref'))) { console.warn( 'RAST list jobs backend returned selectall_arrayref errors. ' @@ -664,8 +714,7 @@ export async function listRastGenomes(): Promise { mod_time: job.mod_time ? String(job.mod_time) : undefined, type: 'Genome', } satisfies RastGenomeJob; - }) - .filter((job) => job.genome_id.length > 0 || job.id.length > 0); + }); } /** From 99184313507d93711080ce256b3787b2a06ead53 Mon Sep 17 00:00:00 2001 From: VibhavSetlur Date: Wed, 13 May 2026 15:47:16 -0500 Subject: [PATCH 5/6] fix(ui): remove hard truncation on reactions table equation column Replaced TruncatedWithTooltip with a flex-based wrapper that allows equation text to wrap naturally. Users can now resize the column and see full equations without ellipsis truncation. --- app/(reference-data)/biochem/reactions/page.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/(reference-data)/biochem/reactions/page.tsx b/app/(reference-data)/biochem/reactions/page.tsx index c6c2f4ee..5c41e597 100644 --- a/app/(reference-data)/biochem/reactions/page.tsx +++ b/app/(reference-data)/biochem/reactions/page.tsx @@ -298,12 +298,13 @@ export default function ReactionsPage() { { field: 'definition', headerName: 'Equation', - width: 350, + flex: 1, + minWidth: 280, sortable: false, renderCell: (params) => ( - + - + ), }, { From 949e7b516db750b183858da348e9931cf2e26dc2 Mon Sep 17 00:00:00 2001 From: VibhavSetlur Date: Wed, 13 May 2026 15:51:52 -0500 Subject: [PATCH 6/6] fix(lint): suppress set-state-in-effect rule in RastGenomePreviewDialog The effect intentionally resets loading/genomeData/error state when open or job changes. This is the standard pattern for data-fetching effects and the lint rule is over-zealous here. --- components/build-model/RastGenomePreviewDialog.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/components/build-model/RastGenomePreviewDialog.tsx b/components/build-model/RastGenomePreviewDialog.tsx index 6b366fec..fd906d9a 100644 --- a/components/build-model/RastGenomePreviewDialog.tsx +++ b/components/build-model/RastGenomePreviewDialog.tsx @@ -32,24 +32,27 @@ export default function RastGenomePreviewDialog({ open, job, onProceed, onClose if (!open || !job) return; const genomeId = job.genome_id || job.id; - if (!genomeId) { - setError('No genome ID available'); - return; - } + if (!genomeId) return; + let cancelled = false; + // eslint-disable-next-line react-hooks/set-state-in-effect setLoading(true); setGenomeData(null); setError(null); getRastGenomeData(genomeId) .then((data) => { + if (cancelled) return; setGenomeData(data); setLoading(false); }) .catch((err) => { + if (cancelled) return; setError(err instanceof Error ? err.message : 'Failed to fetch genome data'); setLoading(false); }); + + return () => { cancelled = true; }; }, [open, job]); const metadataRows: { label: string; value: string }[] = [];