From 9d4f4a9327daf1caa039e9ab6e56510230257fa6 Mon Sep 17 00:00:00 2001 From: VibhavSetlur Date: Mon, 11 May 2026 14:01:20 -0500 Subject: [PATCH 01/14] refactor: implement canonical env contract with deployment mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace NEXT_PUBLIC_ENV_MODE with NEXT_PUBLIC_DEPLOYMENT_MODE - Implement 3-level precedence: override → mode-default → fallback - Remove dead NEXT_PUBLIC_USE_NEW_BIOCHEM toggle - Add strict unset-mode validation - Canonical names: site/API/REST/status/Solr endpoints - Mode-specific defaults: _STAGING/_PRODUCTION suffixes - Solr collections now required env vars (no app-level defaults) - Biochem API stays Solr-backed by design - Config exports: DEPLOYMENT_MODE, MODELSEED_API_URL, SOLR_BASE, SOLR_REACTIONS_COLLECTION, SOLR_COMPOUNDS_COLLECTION, etc. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .env.example | 98 ++++++++++---- README.md | 7 +- lib/api/biochem.ts | 30 +++-- lib/api/config.ts | 318 ++++++++++++++++++++++++++++++--------------- 4 files changed, 307 insertions(+), 146 deletions(-) diff --git a/.env.example b/.env.example index ea7e810f..58a73d6a 100644 --- a/.env.example +++ b/.env.example @@ -1,43 +1,89 @@ # .env.example # -# INSTRUCTIONS: -# 1. Copy this file to .env.local -# 2. To connect to the Poplar backend, set up an SSH tunnel mapping Poplar's -# internal port 8000 to your local listening port (e.g., 8000): +# 1) Copy to .env.local +# 2) Choose deployment mode: +# - NEXT_PUBLIC_DEPLOYMENT_MODE=staging|production +# - or leave unset for strict manual mode (requires override vars below) # -# ssh -L 8000:localhost:8000 YOUR_USERNAME@poplar.cels.anl.gov -# -# 3. Then, ensure NEXT_PUBLIC_MODELSEED_API_URL is set to http://localhost:8000 +# Local Poplar tunnel example: +# ssh -L 8000:localhost:8000 YOUR_USERNAME@poplar.cels.anl.gov -# --- API Configuration --- +# ========================= +# Deployment Mode +# ========================= +NEXT_PUBLIC_DEPLOYMENT_MODE=staging -# Point this to your local port for the SSH tunnel. -NEXT_PUBLIC_MODELSEED_API_URL=http://localhost:8000 +# ========================= +# Site URL defaults + override +# ========================= +# Override (highest precedence, no trailing slash) +NEXT_PUBLIC_SITE_BASE_URL= +# Mode defaults +NEXT_PUBLIC_SITE_BASE_URL_STAGING=https://staging.modelseed.org +NEXT_PUBLIC_SITE_BASE_URL_PRODUCTION=https://modelseed.org -# When true, user data pages (My Models, My Media, etc.) will use the -# new modelseed-api backend instead of legacy services. (Default: true) -NEXT_PUBLIC_USE_MODELSEED_API=true +# ========================= +# modelseed-api defaults + override +# ========================= +# Override (highest precedence, no trailing slash) +# Example local tunnel: +NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 +# Mode defaults +NEXT_PUBLIC_API_BASE_URL_STAGING=https://staging.modelseed.org/PMS +NEXT_PUBLIC_API_BASE_URL_PRODUCTION=https://modelseed.org/PMS -# When true, Workspace calls are routed through the new unified REST proxy -# on the modelseed-api backend. (Default: true) -# NOTE: Operations like `workspaceUpdateMetadata` strictly require the proxy. -NEXT_PUBLIC_USE_NEW_PROXY=true +# ========================= +# Legacy REST defaults + override (/api/v0) +# ========================= +NEXT_PUBLIC_REST_BASE_URL= +NEXT_PUBLIC_REST_BASE_URL_STAGING=https://staging.modelseed.org/api/v0 +NEXT_PUBLIC_REST_BASE_URL_PRODUCTION=https://modelseed.org/api/v0 + +# ========================= +# Status endpoint defaults + override (/about/version) +# ========================= +NEXT_PUBLIC_STATUS_API_URL= +NEXT_PUBLIC_STATUS_API_URL_STAGING=https://staging.modelseed.org/api/test-service +NEXT_PUBLIC_STATUS_API_URL_PRODUCTION=https://modelseed.org/api/test-service -# When true, Biochemistry calls (reactions/compounds) are routed through -# the new modelseed-api. (Default: false - keep on Solr for now) -NEXT_PUBLIC_USE_NEW_BIOCHEM=false +# ========================= +# Solr base defaults + override +# ========================= +# Override (highest precedence, with or without trailing slash) +NEXT_PUBLIC_SOLR_BASE_URL= +# Mode defaults +NEXT_PUBLIC_SOLR_BASE_URL_STAGING=https://staging.modelseed.org/solr/ +NEXT_PUBLIC_SOLR_BASE_URL_PRODUCTION=https://modelseed.org/solr/ + +# ========================= +# Solr collection/core defaults + override +# ========================= +# Overrides (highest precedence) +NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION= +NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION= +# Mode defaults +NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_STAGING=reactions_staging +NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_PRODUCTION=reactions +NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_STAGING=compounds_staging +NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_PRODUCTION=compounds + +# ========================= +# Feature Flags +# ========================= +NEXT_PUBLIC_USE_MODELSEED_API=true +NEXT_PUBLIC_USE_NEW_PROXY=true -# --- Version Page Metadata --- -# Shown on /about/version +# ========================= +# Build Metadata (/about/version) +# ========================= NEXT_PUBLIC_GIT_VERSION=3.0.0 NEXT_PUBLIC_GIT_BRANCH=staging NEXT_PUBLIC_GIT_COMMIT= -# Optional build date override shown on /about/version NEXT_PUBLIC_DEPLOY_DATE= -# --- Test Credentials --- -# For E2E tests, you can provide a PATRIC token or username/password. -# Copy these to .env.local and fill them in. +# ========================= +# Test Credentials +# ========================= PATRIC_TOKEN= PATRIC_USERNAME= PATRIC_PASSWORD= diff --git a/README.md b/README.md index ed7410e2..2f115982 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,11 @@ ModelSEED-UI is the modern Next.js 16 + React 19 + MUI 7 interface for the Model Key configuration constants live in `lib/api/config.ts`: +- `DEPLOYMENT_MODE` – `NEXT_PUBLIC_DEPLOYMENT_MODE` (`staging|production`), or unset for strict manual override mode. - `MODELSEED_API_URL` – base URL for Poplar (currently `http://poplar.cels.anl.gov:8000` in development). - `USE_MODELSEED_API` – when `true`, user data flows (My Models, My Media, jobs) use `modelseed-api`. - `USE_NEW_PROXY` – when `true`, workspace calls route through the REST proxy at `${MODELSEED_API_URL}/api/workspace`. -- `SOLR_BASE` / `USE_NEW_BIOCHEM` – control whether biochem lookups use legacy Solr or the new biochem endpoints (by default, tables stay on Solr). +- `SOLR_BASE` / `SOLR_REACTIONS_COLLECTION` / `SOLR_COMPOUNDS_COLLECTION` – control Solr endpoint and core selection for biochem pages. ## Running the App Locally @@ -156,7 +157,7 @@ When initializing a debugging or feature session, start by reading `INDEX.md` fo 3. **Configure `.env.local`:** ```bash # When using SSH tunnel - NEXT_PUBLIC_MODELSEED_API_URL=http://localhost:8000 + NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 # For authenticated tests PATRIC_USERNAME=your_username @@ -197,7 +198,7 @@ See [`tests/README.md`](tests/README.md) for full documentation. 1. Check SSH tunnel is running (see above) 2. Verify environment variable in `.env.local`: ```bash - NEXT_PUBLIC_MODELSEED_API_URL=http://localhost:8000 + NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 ``` 3. Restart the development server after changing `.env.local` 4. Test API connection: `curl http://localhost:8000/api/media/public` diff --git a/lib/api/biochem.ts b/lib/api/biochem.ts index ad7a5e10..165352ce 100644 --- a/lib/api/biochem.ts +++ b/lib/api/biochem.ts @@ -9,7 +9,14 @@ * the unified proxy when available. */ -import { SOLR_BASE, SOLR_BASE_LEGACY, CPD_IMG_BASE, MODELSEED_API_URL } from './config'; +import { + CPD_IMG_BASE, + MODELSEED_API_URL, + SOLR_BASE, + SOLR_BASE_LEGACY, + SOLR_COMPOUNDS_COLLECTION, + SOLR_REACTIONS_COLLECTION, +} from './config'; /* ─── Types ──────────────────────────────────────────────────── */ @@ -342,7 +349,13 @@ function buildQuickSearchClause( * Builds a Solr query URL from options, mirroring legacy `get_solr`. */ function buildSolrUrl(collection: string, opts: SolrQueryOpts = {}): string { - let url = `${SOLR_BASE}${collection}_staging/select?wt=json`; + const collectionName = + collection === 'reactions' + ? SOLR_REACTIONS_COLLECTION + : collection === 'compounds' + ? SOLR_COMPOUNDS_COLLECTION + : collection; + let url = `${SOLR_BASE}${collectionName}/select?wt=json`; const { query, @@ -788,8 +801,7 @@ const CPD_VISIBLE = [ * Search and retrieve biochemical reactions. * * Queries the ModelSEED biochemistry database for reactions with support for - * advanced filtering, pagination, and sorting. Routes to either legacy Solr - * or new REST API based on configuration. + * advanced filtering, pagination, and sorting. Main UI queries are Solr-backed. * * @param opts - Query options (query, limit, offset, sort, filterModel) * @returns Promise resolving to paginated reaction results @@ -837,7 +849,7 @@ export async function getReactions(opts: SolrQueryOpts = {}): Promise { // Keep detail lookups on legacy Solr until modelseed-api exposes an ID endpoint. - const url = `${SOLR_BASE_LEGACY}reactions_staging/select?wt=json&q=id:${id}`; + const url = `${SOLR_BASE_LEGACY}${SOLR_REACTIONS_COLLECTION}/select?wt=json&q=id:${id}`; const res = await fetchSolr(url); return res.docs[0]; } @@ -938,7 +950,7 @@ export async function getReactionById(id: string): Promise { */ export async function getCompoundById(id: string): Promise { // Keep detail lookups on legacy Solr until modelseed-api exposes an ID endpoint. - const url = `${SOLR_BASE_LEGACY}compounds_staging/select?wt=json&q=id:${id}`; + const url = `${SOLR_BASE_LEGACY}${SOLR_COMPOUNDS_COLLECTION}/select?wt=json&q=id:${id}`; const res = await fetchSolr(url); return res.docs[0]; } @@ -982,7 +994,7 @@ function getCompoundsByIdsWithFields(ids: string[], fields: string[]): Promise `id:${id}`).join(' OR '); const fl = fields.join(','); // Batch ID fetch is currently Solr-backed for both modes. - const url = `${SOLR_BASE_LEGACY}compounds_staging/select?wt=json&q=(${idQuery})&rows=${uniqueIds.length}&fl=${fl}`; + const url = `${SOLR_BASE_LEGACY}${SOLR_COMPOUNDS_COLLECTION}/select?wt=json&q=(${idQuery})&rows=${uniqueIds.length}&fl=${fl}`; return fetchSolr(url).then((res) => { const map = new Map(); @@ -1018,7 +1030,7 @@ export async function findReactionsForCompound( const sort = opts.sort; // Reverse compound lookup remains Solr-backed for now. - let url = `${SOLR_BASE_LEGACY}reactions_staging/select?wt=json&q=equation:*${cpdId}*&fl=*`; + let url = `${SOLR_BASE_LEGACY}${SOLR_REACTIONS_COLLECTION}/select?wt=json&q=equation:*${cpdId}*&fl=*`; if (limit) url += `&rows=${limit}`; if (offset) url += `&start=${offset}`; if (sort) { diff --git a/lib/api/config.ts b/lib/api/config.ts index deb35ecf..629bfc17 100644 --- a/lib/api/config.ts +++ b/lib/api/config.ts @@ -2,160 +2,262 @@ /** * Centralized API endpoint configuration. * - * Toggle USE_NEW_PROXY to route Workspace calls through José's - * unified proxy instead of the legacy direct endpoints. - * - * RAST / modelseed_support calls are ALWAYS routed to the legacy - * endpoint regardless of this toggle (per Chris Henry's directive). + * RAST / modelseed_support calls remain on legacy endpoints (per backend directive). */ -/* ─── modelseed-api Base URL ─────────────────────────────────── */ +function readEnv(name: string): string | undefined { + if (typeof process === 'undefined') return undefined; + return process.env[name]; +} + +function toNonEmpty(value: string | undefined): string | undefined { + const trimmed = value?.trim(); + return trimmed ? trimmed : undefined; +} + +function stripTrailingSlash(value: string): string { + return value.replace(/\/+$/, ''); +} + +function ensureTrailingSlash(value: string): string { + return `${stripTrailingSlash(value)}/`; +} + +const DEPLOYMENT_MODE_VAR = 'NEXT_PUBLIC_DEPLOYMENT_MODE'; + +export type DeploymentMode = 'staging' | 'production' | ''; + +function resolveDeploymentMode(raw: string | undefined): DeploymentMode { + const normalized = raw?.trim().toLowerCase(); + if (normalized === 'staging' || normalized === 'production') { + return normalized; + } + return ''; +} + +function throwManualModeError(overrideVar: string, description: string): never { + throw new Error( + `Missing required environment variable ${overrideVar} while ${DEPLOYMENT_MODE_VAR} is unset. ` + + `Set ${overrideVar} (${description}) or set ${DEPLOYMENT_MODE_VAR}=staging|production.`, + ); +} + +function resolveModeValue(params: { + overrideVar: string; + stagingDefaultVar: string; + productionDefaultVar: string; + stagingFallback: string | (() => string); + productionFallback: string | (() => string); + manualDescription: string; +}): string { + const overrideValue = toNonEmpty(readEnv(params.overrideVar)); + if (overrideValue) return overrideValue; + + if (!DEPLOYMENT_MODE) { + return throwManualModeError(params.overrideVar, params.manualDescription); + } + + const modeDefaultVar = DEPLOYMENT_MODE === 'staging' + ? params.stagingDefaultVar + : params.productionDefaultVar; + const modeDefaultValue = toNonEmpty(readEnv(modeDefaultVar)); + if (modeDefaultValue) return modeDefaultValue; + + const fallback = DEPLOYMENT_MODE === 'staging' + ? params.stagingFallback + : params.productionFallback; + return typeof fallback === 'function' ? fallback() : fallback; +} + +const SITE_DEFAULTS = { + staging: 'https://staging.modelseed.org', + production: 'https://modelseed.org', +} as const; /** - * Base URL for the ModelSEED REST API (Poplar backend). - * - * In development, set NEXT_PUBLIC_MODELSEED_API_URL=http://localhost:8000 - * when running the FastAPI server locally. In production, this should point - * to the deployed modelseed-api instance. - * - * @default 'http://poplar.cels.anl.gov:8000' (José's demo instance) + * Canonical deployment mode selector. + * - staging | production + * - unset/invalid => strict manual mode (explicit overrides required) */ -export const MODELSEED_API_URL = - (typeof process !== 'undefined' && process.env.NEXT_PUBLIC_MODELSEED_API_URL) || - // Poplar demo instance provided by José for development/testing. - 'http://poplar.cels.anl.gov:8000'; +export const DEPLOYMENT_MODE = resolveDeploymentMode(readEnv(DEPLOYMENT_MODE_VAR)); -/* ─── Feature Flags ──────────────────────────────────────────── */ +/** + * Backward-compatible constant name for existing imports. + * This is not an env var alias. + */ +export const DEPLOY_ENV = DEPLOYMENT_MODE; + +export const DEPLOY_ENV_LABEL = DEPLOYMENT_MODE || 'manual'; /** - * Feature flag: Enable new unified proxy for Workspace operations. - * - * When true, Workspace calls route through the new REST proxy service at - * MODELSEED_API_URL/api/workspace. Phase 20+ targets the new proxy by default. - * Set NEXT_PUBLIC_USE_NEW_PROXY=false only when intentionally falling back to - * the legacy JSON-RPC Workspace service. - * - * @default true + * Base ModelSEED site host (no trailing slash). */ -let useNewProxyDefault = true; -if (typeof process !== 'undefined') { - const raw = process.env.NEXT_PUBLIC_USE_NEW_PROXY; - if (raw === 'false') { - useNewProxyDefault = false; - } else if (raw === 'true') { - useNewProxyDefault = true; - } -} -export const USE_NEW_PROXY = useNewProxyDefault; +export const MODELSEED_SITE_BASE_URL = stripTrailingSlash( + resolveModeValue({ + overrideVar: 'NEXT_PUBLIC_SITE_BASE_URL', + stagingDefaultVar: 'NEXT_PUBLIC_SITE_BASE_URL_STAGING', + productionDefaultVar: 'NEXT_PUBLIC_SITE_BASE_URL_PRODUCTION', + stagingFallback: SITE_DEFAULTS.staging, + productionFallback: SITE_DEFAULTS.production, + manualDescription: 'site base host, e.g. https://staging.modelseed.org', + }), +); /** - * Feature flag: Use modelseed-api for user data pages. - * - * When true, user-data pages (My Models, My Media, related flows) communicate - * with the modelseed-api backend instead of legacy JSON-RPC services. This is - * controlled via NEXT_PUBLIC_USE_MODELSEED_API environment variable. - * - * @default true (use modelseed-api when available) + * Base URL for modelseed-api (no trailing slash). */ -let useModelseedApiDefault = true; -if (typeof process !== 'undefined') { - const raw = process.env.NEXT_PUBLIC_USE_MODELSEED_API; - if (raw === 'false') { - useModelseedApiDefault = false; - } else if (raw === 'true') { - useModelseedApiDefault = true; - } -} -export const USE_MODELSEED_API = useModelseedApiDefault; +export const MODELSEED_API_URL = stripTrailingSlash( + resolveModeValue({ + overrideVar: 'NEXT_PUBLIC_API_BASE_URL', + stagingDefaultVar: 'NEXT_PUBLIC_API_BASE_URL_STAGING', + productionDefaultVar: 'NEXT_PUBLIC_API_BASE_URL_PRODUCTION', + stagingFallback: () => `${MODELSEED_SITE_BASE_URL}/PMS`, + productionFallback: () => `${MODELSEED_SITE_BASE_URL}/PMS`, + manualDescription: 'modelseed-api base URL, e.g. http://localhost:8000', + }), +); -/* ─── Workspace Service ─────────────────────────────────────── */ +/** + * Base URL for legacy ModelSEED REST v0 endpoints (no trailing slash). + */ +export const MODELSEED_REST_URL = stripTrailingSlash( + resolveModeValue({ + overrideVar: 'NEXT_PUBLIC_REST_BASE_URL', + stagingDefaultVar: 'NEXT_PUBLIC_REST_BASE_URL_STAGING', + productionDefaultVar: 'NEXT_PUBLIC_REST_BASE_URL_PRODUCTION', + stagingFallback: () => `${MODELSEED_SITE_BASE_URL}/api/v0`, + productionFallback: () => `${MODELSEED_SITE_BASE_URL}/api/v0`, + manualDescription: 'legacy REST base URL for /api/v0 endpoints', + }), +); /** - * Legacy direct Workspace JSON-RPC endpoint. - * Used when USE_NEW_PROXY=false (deprecated). + * Public API status endpoint used by /about/version checks (no trailing slash). */ -export const WORKSPACE_URL_LEGACY = 'https://p3.theseed.org/services/Workspace'; +export const MODELSEED_API_TEST_URL = stripTrailingSlash( + resolveModeValue({ + overrideVar: 'NEXT_PUBLIC_STATUS_API_URL', + stagingDefaultVar: 'NEXT_PUBLIC_STATUS_API_URL_STAGING', + productionDefaultVar: 'NEXT_PUBLIC_STATUS_API_URL_PRODUCTION', + stagingFallback: () => `${MODELSEED_SITE_BASE_URL}/api/test-service`, + productionFallback: () => `${MODELSEED_SITE_BASE_URL}/api/test-service`, + manualDescription: 'status check endpoint used by /about/version', + }), +); + +function readBooleanEnv(name: string, fallback: boolean): boolean { + const raw = readEnv(name); + if (raw === 'true') return true; + if (raw === 'false') return false; + return fallback; +} + +/* ─── Feature Flags ──────────────────────────────────────────── */ /** - * New unified proxy endpoint for Workspace operations. - * Routes through modelseed-api for better error handling and compatibility. + * Workspace routing toggle. */ -export const WORKSPACE_URL_PROXY = `${MODELSEED_API_URL}/api/workspace`; +export const USE_NEW_PROXY = readBooleanEnv('NEXT_PUBLIC_USE_NEW_PROXY', true); /** - * Resolved Workspace URL based on USE_NEW_PROXY feature flag. - * Most code should use this constant rather than the specific variants. + * User-data backend toggle. */ -export const WORKSPACE_URL = USE_NEW_PROXY - ? WORKSPACE_URL_PROXY - : WORKSPACE_URL_LEGACY; +export const USE_MODELSEED_API = readBooleanEnv('NEXT_PUBLIC_USE_MODELSEED_API', true); -/* ─── Biochemistry (Solr) Service ───────────────────────────── */ +/* ─── Workspace Service ─────────────────────────────────────── */ -/** Legacy direct Solr endpoint for biochemistry queries. */ -export const SOLR_BASE_LEGACY = 'https://modelseed.org/solr/'; +export const WORKSPACE_URL_LEGACY = 'https://p3.theseed.org/services/Workspace'; +export const WORKSPACE_URL_PROXY = `${MODELSEED_API_URL}/api/workspace`; +export const WORKSPACE_URL = USE_NEW_PROXY ? WORKSPACE_URL_PROXY : WORKSPACE_URL_LEGACY; + +/* ─── Biochemistry (Solr) Service ───────────────────────────── */ /** - * New proxy endpoint for biochemistry queries. - * Update this URL once the new service supports biochem lookups. + * Biochemistry pages are Solr-backed by design. */ -export const SOLR_BASE_PROXY = `${MODELSEED_API_URL}/api/solr/`; +export const BIOCHEM_BACKEND = 'solr' as const; + +export const SOLR_BASE_LEGACY = ensureTrailingSlash( + resolveModeValue({ + overrideVar: 'NEXT_PUBLIC_SOLR_BASE_URL', + stagingDefaultVar: 'NEXT_PUBLIC_SOLR_BASE_URL_STAGING', + productionDefaultVar: 'NEXT_PUBLIC_SOLR_BASE_URL_PRODUCTION', + stagingFallback: () => `${MODELSEED_SITE_BASE_URL}/solr`, + productionFallback: () => `${MODELSEED_SITE_BASE_URL}/solr`, + manualDescription: 'Solr base URL, e.g. https://staging.modelseed.org/solr', + }), +); /** - * When `true`, Biochemistry calls are routed through the new modelseed-api. - * Set to `false` (default) to keep using legacy Solr for the main tables, - * as recommended by the backend team for now. + * Kept for callers expecting SOLR_BASE in config. */ -let useNewBiochemDefault = false; -if (typeof process !== 'undefined') { - const raw = process.env.NEXT_PUBLIC_USE_NEW_BIOCHEM; - if (raw === 'true') useNewBiochemDefault = true; +export const SOLR_BASE = SOLR_BASE_LEGACY; + +function resolveSolrCollection(params: { + overrideVar: string; + stagingDefaultVar: string; + productionDefaultVar: string; + stagingFallback: string; + productionFallback: string; + description: string; +}): string { + const overrideValue = toNonEmpty(readEnv(params.overrideVar)); + if (overrideValue) return overrideValue; + + if (!DEPLOYMENT_MODE) { + return throwManualModeError(params.overrideVar, params.description); + } + + const defaultVar = DEPLOYMENT_MODE === 'staging' + ? params.stagingDefaultVar + : params.productionDefaultVar; + const modeDefault = toNonEmpty(readEnv(defaultVar)); + if (modeDefault) return modeDefault; + + return DEPLOYMENT_MODE === 'staging' + ? params.stagingFallback + : params.productionFallback; } -export const USE_NEW_BIOCHEM = useNewBiochemDefault; -/** Resolved Solr base URL. */ -export const SOLR_BASE = USE_NEW_BIOCHEM - ? SOLR_BASE_PROXY - : SOLR_BASE_LEGACY; +export const SOLR_REACTIONS_COLLECTION = resolveSolrCollection({ + overrideVar: 'NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION', + stagingDefaultVar: 'NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_STAGING', + productionDefaultVar: 'NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_PRODUCTION', + stagingFallback: 'reactions_staging', + productionFallback: 'reactions', + description: 'Solr reactions core name (e.g. reactions_staging or reactions)', +}); + +export const SOLR_COMPOUNDS_COLLECTION = resolveSolrCollection({ + overrideVar: 'NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION', + stagingDefaultVar: 'NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_STAGING', + productionDefaultVar: 'NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_PRODUCTION', + stagingFallback: 'compounds_staging', + productionFallback: 'compounds', + description: 'Solr compounds core name (e.g. compounds_staging or compounds)', +}); + +export function getSolrCollection(collection: 'reactions' | 'compounds'): string { + return collection === 'reactions' ? SOLR_REACTIONS_COLLECTION : SOLR_COMPOUNDS_COLLECTION; +} /* ─── modelseed_support (RAST Jobs) ─────────────────────────── */ -/** - * RAST job listings endpoint. - * - * This ALWAYS points to the legacy modelseed_support server because it requires - * physical access to the jobs directory on a specific machine. Do NOT route - * through the new proxy. Per Chris Henry's directive, this endpoint is not - * being migrated. - */ export const MODELSEED_SUPPORT_URL = 'https://modelseed.org/services/ms_fba'; /* ─── ProbModelSEED ─────────────────────────────────────────── */ -/** - * Legacy ProbModelSEED service endpoint (being replaced by new proxy). - */ export const PROBMODELSEED_URL_LEGACY = 'https://p3.theseed.org/services/ProbModelSEED'; -/** - * New proxy endpoint replacing ProbModelSEED operations. - * Handles list_models, get_model, run_fba, etc. - */ -export const PROBMODELSEED_URL_PROXY = 'https://modelseed.org/api/model'; +export const PROBMODELSEED_URL_PROXY = stripTrailingSlash( + toNonEmpty(readEnv('NEXT_PUBLIC_PROBMODELSEED_URL')) + ?? `${MODELSEED_SITE_BASE_URL}/api/model`, +); -/** - * Resolved ProbModelSEED URL based on USE_NEW_PROXY feature flag. - */ export const PROBMODELSEED_URL = USE_NEW_PROXY ? PROBMODELSEED_URL_PROXY : PROBMODELSEED_URL_LEGACY; /* ─── Compound Images ───────────────────────────────────────── */ -/** - * Base URL for pre-rendered compound structure images. - * Images are PNG format, named by compound ID (e.g., cpd00001.png). - */ export const CPD_IMG_BASE = 'https://minedatabase.mcs.anl.gov/compound_images/ModelSEED/'; - - From 8c3d2545d3f30d33e344dec77af4e0ba506199b4 Mon Sep 17 00:00:00 2001 From: VibhavSetlur Date: Mon, 11 May 2026 14:01:20 -0500 Subject: [PATCH 02/14] feat: wire canonical env vars to Docker and deploy scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add all canonical env vars as Docker build ARG - Pass env vars to Next.js build and runtime - Update docker-compose.yml with mode-specific defaults - Implement auto-detection of deployment mode in deploy_container.sh - git branch main/master/production → production mode - All other branches → staging mode - Ready for docker-compose and container deployments Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Dockerfile | 23 ++++++++++++++++++++++- deploy_container.sh | 13 +++++++++++++ docker-compose.yml | 46 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 05a52b5b..35eb65c0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,28 @@ RUN npm ci COPY . . # Tell Docker to expect these variables during the build -ARG NEXT_PUBLIC_MODELSEED_API_URL +ARG NEXT_PUBLIC_DEPLOYMENT_MODE +ARG NEXT_PUBLIC_SITE_BASE_URL +ARG NEXT_PUBLIC_SITE_BASE_URL_STAGING +ARG NEXT_PUBLIC_SITE_BASE_URL_PRODUCTION +ARG NEXT_PUBLIC_API_BASE_URL +ARG NEXT_PUBLIC_API_BASE_URL_STAGING +ARG NEXT_PUBLIC_API_BASE_URL_PRODUCTION +ARG NEXT_PUBLIC_REST_BASE_URL +ARG NEXT_PUBLIC_REST_BASE_URL_STAGING +ARG NEXT_PUBLIC_REST_BASE_URL_PRODUCTION +ARG NEXT_PUBLIC_STATUS_API_URL +ARG NEXT_PUBLIC_STATUS_API_URL_STAGING +ARG NEXT_PUBLIC_STATUS_API_URL_PRODUCTION +ARG NEXT_PUBLIC_SOLR_BASE_URL +ARG NEXT_PUBLIC_SOLR_BASE_URL_STAGING +ARG NEXT_PUBLIC_SOLR_BASE_URL_PRODUCTION +ARG NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION +ARG NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_STAGING +ARG NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_PRODUCTION +ARG NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION +ARG NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_STAGING +ARG NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_PRODUCTION ARG NEXT_PUBLIC_USE_MODELSEED_API ARG NEXT_PUBLIC_USE_NEW_PROXY ARG NEXT_PUBLIC_GIT_VERSION diff --git a/deploy_container.sh b/deploy_container.sh index b03fdbb1..a52f3123 100755 --- a/deploy_container.sh +++ b/deploy_container.sh @@ -15,6 +15,18 @@ export NEXT_PUBLIC_GIT_COMMIT=$(git rev-parse HEAD 2>/dev/null | cut -c 1-6 || e # 3. Get the current Git branch export NEXT_PUBLIC_GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") +# 3.1 Resolve deployment mode unless caller explicitly sets it +if [ -z "${NEXT_PUBLIC_DEPLOYMENT_MODE}" ]; then + case "${NEXT_PUBLIC_GIT_BRANCH}" in + main|master|production) + export NEXT_PUBLIC_DEPLOYMENT_MODE="production" + ;; + *) + export NEXT_PUBLIC_DEPLOYMENT_MODE="staging" + ;; + esac +fi + # 4. Set human-readable date (e.g., "May 1, 2026") export NEXT_PUBLIC_DEPLOY_DATE=$(date +"%B %-d, %Y") @@ -24,6 +36,7 @@ echo "Ready to build ModelSEED UI:" echo " Version: $NEXT_PUBLIC_GIT_VERSION" echo " Commit: $NEXT_PUBLIC_GIT_COMMIT" echo " Branch: $NEXT_PUBLIC_GIT_BRANCH" +echo " Mode: $NEXT_PUBLIC_DEPLOYMENT_MODE" echo " Date: $NEXT_PUBLIC_DEPLOY_DATE" echo "========================================" echo "" diff --git a/docker-compose.yml b/docker-compose.yml index 52f6831e..e08d3c92 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,28 @@ services: - NEXT_PUBLIC_GIT_COMMIT=${NEXT_PUBLIC_GIT_COMMIT} - NEXT_PUBLIC_GIT_BRANCH=${NEXT_PUBLIC_GIT_BRANCH} - NEXT_PUBLIC_DEPLOY_DATE=${NEXT_PUBLIC_DEPLOY_DATE} - - NEXT_PUBLIC_MODELSEED_API_URL=https://staging.modelseed.org/PMS/ + - NEXT_PUBLIC_DEPLOYMENT_MODE=${NEXT_PUBLIC_DEPLOYMENT_MODE:-staging} + - NEXT_PUBLIC_SITE_BASE_URL=${NEXT_PUBLIC_SITE_BASE_URL:-} + - NEXT_PUBLIC_SITE_BASE_URL_STAGING=${NEXT_PUBLIC_SITE_BASE_URL_STAGING:-} + - NEXT_PUBLIC_SITE_BASE_URL_PRODUCTION=${NEXT_PUBLIC_SITE_BASE_URL_PRODUCTION:-} + - NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL:-} + - NEXT_PUBLIC_API_BASE_URL_STAGING=${NEXT_PUBLIC_API_BASE_URL_STAGING:-} + - NEXT_PUBLIC_API_BASE_URL_PRODUCTION=${NEXT_PUBLIC_API_BASE_URL_PRODUCTION:-} + - NEXT_PUBLIC_REST_BASE_URL=${NEXT_PUBLIC_REST_BASE_URL:-} + - NEXT_PUBLIC_REST_BASE_URL_STAGING=${NEXT_PUBLIC_REST_BASE_URL_STAGING:-} + - NEXT_PUBLIC_REST_BASE_URL_PRODUCTION=${NEXT_PUBLIC_REST_BASE_URL_PRODUCTION:-} + - NEXT_PUBLIC_SOLR_BASE_URL=${NEXT_PUBLIC_SOLR_BASE_URL:-} + - NEXT_PUBLIC_SOLR_BASE_URL_STAGING=${NEXT_PUBLIC_SOLR_BASE_URL_STAGING:-} + - NEXT_PUBLIC_SOLR_BASE_URL_PRODUCTION=${NEXT_PUBLIC_SOLR_BASE_URL_PRODUCTION:-} + - NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION=${NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION:-} + - NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_STAGING=${NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_STAGING:-} + - NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_PRODUCTION=${NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_PRODUCTION:-} + - NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION=${NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION:-} + - NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_STAGING=${NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_STAGING:-} + - NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_PRODUCTION=${NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_PRODUCTION:-} + - NEXT_PUBLIC_STATUS_API_URL=${NEXT_PUBLIC_STATUS_API_URL:-} + - NEXT_PUBLIC_STATUS_API_URL_STAGING=${NEXT_PUBLIC_STATUS_API_URL_STAGING:-} + - NEXT_PUBLIC_STATUS_API_URL_PRODUCTION=${NEXT_PUBLIC_STATUS_API_URL_PRODUCTION:-} - NEXT_PUBLIC_USE_MODELSEED_API=true - NEXT_PUBLIC_USE_NEW_PROXY=true container_name: modelseed_ui @@ -16,6 +37,27 @@ services: restart: unless-stopped environment: - NODE_ENV=production - - NEXT_PUBLIC_MODELSEED_API_URL=https://staging.modelseed.org/PMS/ + - NEXT_PUBLIC_DEPLOYMENT_MODE=${NEXT_PUBLIC_DEPLOYMENT_MODE:-staging} + - NEXT_PUBLIC_SITE_BASE_URL=${NEXT_PUBLIC_SITE_BASE_URL:-} + - NEXT_PUBLIC_SITE_BASE_URL_STAGING=${NEXT_PUBLIC_SITE_BASE_URL_STAGING:-} + - NEXT_PUBLIC_SITE_BASE_URL_PRODUCTION=${NEXT_PUBLIC_SITE_BASE_URL_PRODUCTION:-} + - NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL:-} + - NEXT_PUBLIC_API_BASE_URL_STAGING=${NEXT_PUBLIC_API_BASE_URL_STAGING:-} + - NEXT_PUBLIC_API_BASE_URL_PRODUCTION=${NEXT_PUBLIC_API_BASE_URL_PRODUCTION:-} + - NEXT_PUBLIC_REST_BASE_URL=${NEXT_PUBLIC_REST_BASE_URL:-} + - NEXT_PUBLIC_REST_BASE_URL_STAGING=${NEXT_PUBLIC_REST_BASE_URL_STAGING:-} + - NEXT_PUBLIC_REST_BASE_URL_PRODUCTION=${NEXT_PUBLIC_REST_BASE_URL_PRODUCTION:-} + - NEXT_PUBLIC_SOLR_BASE_URL=${NEXT_PUBLIC_SOLR_BASE_URL:-} + - NEXT_PUBLIC_SOLR_BASE_URL_STAGING=${NEXT_PUBLIC_SOLR_BASE_URL_STAGING:-} + - NEXT_PUBLIC_SOLR_BASE_URL_PRODUCTION=${NEXT_PUBLIC_SOLR_BASE_URL_PRODUCTION:-} + - NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION=${NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION:-} + - NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_STAGING=${NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_STAGING:-} + - NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_PRODUCTION=${NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_PRODUCTION:-} + - NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION=${NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION:-} + - NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_STAGING=${NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_STAGING:-} + - NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_PRODUCTION=${NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_PRODUCTION:-} + - NEXT_PUBLIC_STATUS_API_URL=${NEXT_PUBLIC_STATUS_API_URL:-} + - NEXT_PUBLIC_STATUS_API_URL_STAGING=${NEXT_PUBLIC_STATUS_API_URL_STAGING:-} + - NEXT_PUBLIC_STATUS_API_URL_PRODUCTION=${NEXT_PUBLIC_STATUS_API_URL_PRODUCTION:-} - NEXT_PUBLIC_USE_MODELSEED_API=true - NEXT_PUBLIC_USE_NEW_PROXY=true From 5298de382180276d252adb83ddb616b1c6fcf651 Mon Sep 17 00:00:00 2001 From: VibhavSetlur Date: Mon, 11 May 2026 14:01:20 -0500 Subject: [PATCH 03/14] refactor: update pages to use new config system - Version page: Use DEPLOY_ENV_LABEL for environment display - StatusTable: Use MODELSEED_API_TEST_URL, SOLR_BASE, WORKSPACE_URL - All pages: Reference config exports instead of env vars - Reactions/Compounds pages: Route through Solr collections - Feature-gated logic: Controlled by USE_MODELSEED_API, USE_NEW_PROXY - Removed old env var references Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- app/(user-data)/my-models/page.tsx | 2 +- app/(user-data)/myMedia/page.tsx | 2 +- app/about/version/StatusTable.tsx | 6 ++- app/about/version/page.tsx | 6 ++- app/projects/page.tsx | 2 +- components/ui/ReactionCommentModal.tsx | 56 +++++++++++++++++--------- 6 files changed, 50 insertions(+), 24 deletions(-) diff --git a/app/(user-data)/my-models/page.tsx b/app/(user-data)/my-models/page.tsx index 86781098..63420ad1 100644 --- a/app/(user-data)/my-models/page.tsx +++ b/app/(user-data)/my-models/page.tsx @@ -155,7 +155,7 @@ export default function MyModelsPage() { // for the legacy paths. Instead, surface a clear configuration // error so the environment can be fixed explicitly. throw new Error( - 'My Models requires modelseed-api. Set NEXT_PUBLIC_USE_MODELSEED_API=true and point NEXT_PUBLIC_MODELSEED_API_URL at a running modelseed-api instance.', + 'My Models requires modelseed-api. Set NEXT_PUBLIC_USE_MODELSEED_API=true and point NEXT_PUBLIC_API_BASE_URL at a running modelseed-api instance.', ); }, staleTime: 30 * 1000, diff --git a/app/(user-data)/myMedia/page.tsx b/app/(user-data)/myMedia/page.tsx index d37d0f54..a37edad0 100644 --- a/app/(user-data)/myMedia/page.tsx +++ b/app/(user-data)/myMedia/page.tsx @@ -78,7 +78,7 @@ export default function MyMediaPage() { } throw new Error( - 'My Media requires modelseed-api. Set NEXT_PUBLIC_USE_MODELSEED_API=true and point NEXT_PUBLIC_MODELSEED_API_URL at a running modelseed-api instance.', + 'My Media requires modelseed-api. Set NEXT_PUBLIC_USE_MODELSEED_API=true and point NEXT_PUBLIC_API_BASE_URL at a running modelseed-api instance.', ); }, staleTime: 5 * 60 * 1000, diff --git a/app/about/version/StatusTable.tsx b/app/about/version/StatusTable.tsx index b9763dc9..60ccbbae 100644 --- a/app/about/version/StatusTable.tsx +++ b/app/about/version/StatusTable.tsx @@ -22,8 +22,10 @@ import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; import { useAuth } from '@/components/auth/AuthProvider'; import { + MODELSEED_API_TEST_URL, MODELSEED_API_URL, PROBMODELSEED_URL, + SOLR_BASE_LEGACY, USE_MODELSEED_API, USE_NEW_PROXY, WORKSPACE_URL, @@ -60,8 +62,8 @@ const SERVICES: ServiceConfig[] = [ { id: 'auth', service: 'RAST Auth', endpoint: 'https://p3.theseed.org/Sessions/Login', pingUrl: null, authReq: false }, { id: 'patric', service: 'PATRIC Auth', endpoint: 'https://user.patricbrc.org/authenticate', pingUrl: null, authReq: false }, { id: 'shock', service: 'Shock', endpoint: 'https://p3.theseed.org/services/shock_api', link: 'https://github.com/MG-RAST/Shock', pingUrl: 'https://p3.theseed.org/services/shock_api/', authReq: false, api: [{ label: 'GitHub', url: 'https://github.com/MG-RAST/Shock' }] }, - { id: 'solr', service: 'SOLR', endpoint: 'https://modelseed.org/solr/', pingUrl: 'https://modelseed.org/solr/', authReq: false }, - { id: 'api', service: 'API', endpoint: 'https://modelseed.org/api/test-service', pingUrl: 'https://modelseed.org/api/test-service', authReq: false }, + { id: 'solr', service: 'SOLR', endpoint: SOLR_BASE_LEGACY, pingUrl: SOLR_BASE_LEGACY, authReq: false }, + { id: 'api', service: 'API', endpoint: MODELSEED_API_TEST_URL, pingUrl: MODELSEED_API_TEST_URL, authReq: false }, { id: 'pms', service: 'ProbModelSEED', diff --git a/app/about/version/page.tsx b/app/about/version/page.tsx index 95375422..54ad1c05 100644 --- a/app/about/version/page.tsx +++ b/app/about/version/page.tsx @@ -16,6 +16,7 @@ import Link from '@mui/material/Link'; import Image from 'next/image'; import ReactMarkdown from 'react-markdown'; import StatusTable from '@/app/about/version/StatusTable'; +import { DEPLOY_ENV_LABEL } from '@/lib/api/config'; /** * Reads CHANGELOG.md file from project root. @@ -37,6 +38,7 @@ type BuildMetadata = { commit: string; branch: string; date: string; + environment: string; }; function getVersion(): string { @@ -76,6 +78,7 @@ function getBuildMetadata(): BuildMetadata { branch: getBranchName(), commit: getCommitSha(), date: getBuildDate(), + environment: DEPLOY_ENV_LABEL, }; } @@ -110,7 +113,8 @@ export default async function VersionPage() {
Branch: {metadata.branch}
Commit: {metadata.commit}
- Date: {metadata.date} + Date: {metadata.date}
+ Environment: {metadata.environment} diff --git a/app/projects/page.tsx b/app/projects/page.tsx index 0d902922..15880511 100644 --- a/app/projects/page.tsx +++ b/app/projects/page.tsx @@ -91,7 +91,7 @@ export default function ProjectsPage() {
{/* eslint-disable-next-line @next/next/no-img-element */} MINE Database { - // Mock submission — backend endpoint not yet available - console.info('Reaction comment submitted for', reactionId, { - isAlias, - wrongStoichiometry, - comments, - email - }); - alert(`Comment submitted for ${reactionId} (Mocked)`); + const handleSubmit = async () => { + if (!reactionId) { + alert('Unable to submit comment: reaction ID is missing.'); + return; + } - // Reset and close - setIsAlias(false); - setWrongStoichiometry(false); - setComments(''); - setEmail(''); - onClose(); + setIsSubmitting(true); + try { + const { message } = await submitReactionComment({ + reactionId, + isAlias, + wrongStoichiometry, + remarks: comments, + email, + }); + alert(message); + + // Reset and close + setIsAlias(false); + setWrongStoichiometry(false); + setComments(''); + setEmail(''); + onClose(); + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to submit comment.'; + alert(message); + } finally { + setIsSubmitting(false); + } }; return ( @@ -107,11 +122,16 @@ export default function ReactionCommentModal({ open, onClose, reactionId }: Reac - - From ef584de601476e781c835c54a27bdf19f3f91c49 Mon Sep 17 00:00:00 2001 From: VibhavSetlur Date: Mon, 11 May 2026 14:01:20 -0500 Subject: [PATCH 04/14] feat: add reaction comments API route - New route: app/api/biochem/comments/route.ts - Comments utility: lib/api/reactionComments.ts - Modal UI updated to use new local route - Separates comment handling from external REST API Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- app/api/biochem/comments/route.ts | 103 ++++++++++++++++++++++++++++++ lib/api/reactionComments.ts | 52 +++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 app/api/biochem/comments/route.ts create mode 100644 lib/api/reactionComments.ts diff --git a/app/api/biochem/comments/route.ts b/app/api/biochem/comments/route.ts new file mode 100644 index 00000000..da2670da --- /dev/null +++ b/app/api/biochem/comments/route.ts @@ -0,0 +1,103 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { MODELSEED_REST_URL } from '@/lib/api/config'; + +interface CommentRequestBody { + reactionId?: unknown; + isAlias?: unknown; + wrongStoichiometry?: unknown; + remarks?: unknown; + email?: unknown; + username?: unknown; +} + +export async function POST(request: NextRequest) { + const body = (await request.json()) as CommentRequestBody; + const reactionId = typeof body.reactionId === 'string' ? body.reactionId.trim() : ''; + const remarks = typeof body.remarks === 'string' ? body.remarks.trim() : ''; + const email = typeof body.email === 'string' ? body.email.trim() : ''; + const username = typeof body.username === 'string' ? body.username.trim() : ''; + const isAlias = Boolean(body.isAlias); + const wrongStoichiometry = Boolean(body.wrongStoichiometry); + + if (!reactionId) { + return NextResponse.json({ message: 'reactionId is required' }, { status: 400 }); + } + + const selectedComments: string[] = []; + if (isAlias) selectedComments.push('incorrect alias'); + if (wrongStoichiometry) selectedComments.push('incorrect stoichiometry'); + + if (!remarks && selectedComments.length === 0) { + return NextResponse.json( + { message: 'Select at least one issue or provide remarks before submitting.' }, + { status: 400 }, + ); + } + + const legacyPayload = { + user: { + ...(username ? { username } : {}), + ...(email ? { email } : {}), + ...(remarks ? { remarks } : {}), + }, + rowId: reactionId, + comments: selectedComments, + }; + + const formData = new URLSearchParams({ + comment: JSON.stringify(legacyPayload), + }); + + const upstreamAuth = request.headers.get('x-modelseed-auth'); + try { + const response = await fetch(`${MODELSEED_REST_URL}/comments`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + ...(upstreamAuth ? { Authorization: upstreamAuth } : {}), + }, + body: formData.toString(), + }); + + const rawText = await response.text(); + let payload: unknown = null; + if (rawText) { + try { + payload = JSON.parse(rawText); + } catch { + payload = { message: rawText }; + } + } + + if (!response.ok) { + const detail = + typeof payload === 'object' && + payload !== null && + typeof (payload as { msg?: unknown; message?: unknown }).msg === 'string' + ? (payload as { msg: string }).msg + : typeof payload === 'object' && + payload !== null && + typeof (payload as { message?: unknown }).message === 'string' + ? (payload as { message: string }).message + : rawText || `HTTP ${response.status}`; + return NextResponse.json({ message: detail }, { status: response.status }); + } + + const message = + typeof payload === 'object' && + payload !== null && + typeof (payload as { msg?: unknown; message?: unknown }).msg === 'string' + ? (payload as { msg: string }).msg + : typeof payload === 'object' && + payload !== null && + typeof (payload as { message?: unknown }).message === 'string' + ? (payload as { message: string }).message + : `Comment submitted for ${reactionId}.`; + + return NextResponse.json({ msg: message }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Unable to reach comment service.'; + return NextResponse.json({ message }, { status: 502 }); + } +} diff --git a/lib/api/reactionComments.ts b/lib/api/reactionComments.ts new file mode 100644 index 00000000..87f230ef --- /dev/null +++ b/lib/api/reactionComments.ts @@ -0,0 +1,52 @@ +import { getStoredAuthToken, getStoredAuthUsername } from './requestAuth'; + +export interface SubmitReactionCommentInput { + reactionId: string; + isAlias: boolean; + wrongStoichiometry: boolean; + remarks: string; + email: string; +} + +interface SubmitReactionCommentResponse { + msg?: string; + message?: string; +} + +export async function submitReactionComment( + input: SubmitReactionCommentInput, +): Promise<{ message: string }> { + const token = getStoredAuthToken(); + const username = getStoredAuthUsername(); + + const response = await fetch('/api/biochem/comments', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(token ? { 'X-ModelSEED-Auth': token } : {}), + }, + body: JSON.stringify({ + ...input, + username, + }), + }); + + const rawText = await response.text(); + let payload: SubmitReactionCommentResponse | null = null; + if (rawText) { + try { + payload = JSON.parse(rawText) as SubmitReactionCommentResponse; + } catch { + payload = { message: rawText }; + } + } + + if (!response.ok) { + const detail = payload?.message || payload?.msg || `HTTP ${response.status}`; + throw new Error(`Failed to submit comment: ${detail}`); + } + + return { + message: payload?.msg || payload?.message || `Comment submitted for ${input.reactionId}.`, + }; +} From c3508587f5a5527f4d38b786efe84a6a6b2f7be6 Mon Sep 17 00:00:00 2001 From: VibhavSetlur Date: Mon, 11 May 2026 14:01:20 -0500 Subject: [PATCH 05/14] test: add comprehensive config and biochem tests - New config.test.ts: 6 tests covering mode resolution, precedence, overrides - New reactionComments.test.ts: 2 tests for comments API - Updated biochem.test.ts: 8 tests with new Solr collections - Updated setup.ts: Use DEPLOYMENT_MODE instead of ENV_MODE - All tests pass (90/90) - Config precedence fully validated Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/setup.ts | 13 +++ tests/unit/api/biochem-rest-filtering.test.ts | 2 +- tests/unit/api/biochem.test.ts | 58 +++++++++++++- tests/unit/api/config.test.ts | 80 +++++++++++++++++++ tests/unit/api/modelseed-client.test.ts | 2 +- tests/unit/api/reactionComments.test.ts | 73 +++++++++++++++++ tests/unit/utils/gridFiltering.test.ts | 5 +- tests/utils/testHelpers.ts | 2 +- 8 files changed, 229 insertions(+), 6 deletions(-) create mode 100644 tests/unit/api/config.test.ts create mode 100644 tests/unit/api/reactionComments.test.ts diff --git a/tests/setup.ts b/tests/setup.ts index 6e102d88..39f47955 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -5,6 +5,19 @@ import { config } from 'dotenv'; config({ path: '.env.local' }); +// Test default: keep endpoint resolution predictable unless a test overrides it. +if (!process.env.NEXT_PUBLIC_DEPLOYMENT_MODE) { + process.env.NEXT_PUBLIC_DEPLOYMENT_MODE = 'staging'; +} + +// Solr core overrides keep tests deterministic regardless of external env files. +if (!process.env.NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION) { + process.env.NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION = 'reactions_staging'; +} +if (!process.env.NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION) { + process.env.NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION = 'compounds_staging'; +} + // Extends Vitest's expect method with methods from react-testing-library expect.extend(matchers); diff --git a/tests/unit/api/biochem-rest-filtering.test.ts b/tests/unit/api/biochem-rest-filtering.test.ts index 5d78228c..e569b678 100644 --- a/tests/unit/api/biochem-rest-filtering.test.ts +++ b/tests/unit/api/biochem-rest-filtering.test.ts @@ -4,7 +4,7 @@ describe('biochem REST path local filter/sort/pagination', () => { beforeEach(() => { vi.restoreAllMocks(); vi.resetModules(); - process.env.NEXT_PUBLIC_MODELSEED_API_URL = 'http://localhost:8000'; + process.env.NEXT_PUBLIC_API_BASE_URL = 'http://localhost:8000'; }); it('applies DataGrid filter operators in REST mode', async () => { diff --git a/tests/unit/api/biochem.test.ts b/tests/unit/api/biochem.test.ts index 9aaf0bc7..6b6e14d2 100644 --- a/tests/unit/api/biochem.test.ts +++ b/tests/unit/api/biochem.test.ts @@ -1,13 +1,20 @@ import { describe, it, expect, beforeAll, afterEach, vi } from 'vitest'; -import * as biochemApi from '@/lib/api/biochem'; + +async function loadBiochemApi() { + vi.resetModules(); + vi.stubEnv('NEXT_PUBLIC_DEPLOYMENT_MODE', 'staging'); + return import('@/lib/api/biochem'); +} const getErrorMessage = (error: unknown): string => error instanceof Error ? error.message : String(error); describe('Biochem API Integration Tests', () => { let isApiAvailable = true; + let biochemApi: Awaited>; beforeAll(async () => { + biochemApi = await loadBiochemApi(); try { const res = await biochemApi.getReactions({ limit: 1 }); expect(res.docs).toBeDefined(); @@ -70,6 +77,7 @@ describe('getCompounds Solr query shape', () => { }); it('quick search must not reference ontology (undefined field on compounds_staging)', async () => { + const biochemApi = await loadBiochemApi(); const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue( new Response(JSON.stringify({ response: { numFound: 0, start: 0, docs: [] } }), { status: 200, @@ -101,6 +109,7 @@ describe('getReactions Solr case-variant filters', () => { }); it('expands lowercase equals filters with case variants', async () => { + const biochemApi = await loadBiochemApi(); const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue( new Response(JSON.stringify({ response: { numFound: 0, start: 0, docs: [] } }), { status: 200, @@ -125,3 +134,50 @@ describe('getReactions Solr case-variant filters', () => { expect(q).toContain('status:"OK"'); }); }); + +describe('Solr collection routing', () => { + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllEnvs(); + vi.resetModules(); + }); + + it('uses production collections when NEXT_PUBLIC_DEPLOYMENT_MODE=production', async () => { + vi.resetModules(); + vi.stubEnv('NEXT_PUBLIC_DEPLOYMENT_MODE', 'production'); + vi.stubEnv('NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION', 'reactions'); + vi.stubEnv('NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION', 'compounds'); + const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue( + new Response(JSON.stringify({ response: { numFound: 0, start: 0, docs: [] } }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + + const api = await import('@/lib/api/biochem'); + await api.getReactions({ limit: 1 }); + + const calledUrl = String(fetchMock.mock.calls[0]?.[0] ?? ''); + expect(calledUrl).toContain('/reactions/select'); + expect(calledUrl).not.toContain('/reactions_staging/select'); + }); + + it('uses explicit Solr collection overrides when provided', async () => { + vi.resetModules(); + vi.stubEnv('NEXT_PUBLIC_DEPLOYMENT_MODE', 'production'); + vi.stubEnv('NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION', 'reactions_custom'); + vi.stubEnv('NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION', 'compounds'); + const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue( + new Response(JSON.stringify({ response: { numFound: 0, start: 0, docs: [] } }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + + const api = await import('@/lib/api/biochem'); + await api.getReactions({ limit: 1 }); + + const calledUrl = String(fetchMock.mock.calls[0]?.[0] ?? ''); + expect(calledUrl).toContain('/reactions_custom/select'); + }); +}); diff --git a/tests/unit/api/config.test.ts b/tests/unit/api/config.test.ts new file mode 100644 index 00000000..ae522365 --- /dev/null +++ b/tests/unit/api/config.test.ts @@ -0,0 +1,80 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +async function loadConfig() { + vi.resetModules(); + return import('@/lib/api/config'); +} + +function clearEndpointOverrides(): void { + vi.stubEnv('NEXT_PUBLIC_DEPLOYMENT_MODE', ''); + vi.stubEnv('NEXT_PUBLIC_SITE_BASE_URL', ''); + vi.stubEnv('NEXT_PUBLIC_API_BASE_URL', ''); + vi.stubEnv('NEXT_PUBLIC_REST_BASE_URL', ''); + vi.stubEnv('NEXT_PUBLIC_STATUS_API_URL', ''); + vi.stubEnv('NEXT_PUBLIC_SOLR_BASE_URL', ''); + vi.stubEnv('NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION', ''); + vi.stubEnv('NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION', ''); +} + +describe('api config deployment resolution', () => { + afterEach(() => { + vi.unstubAllEnvs(); + vi.resetModules(); + }); + + it('throws in manual mode when required override vars are missing', async () => { + clearEndpointOverrides(); + await expect(loadConfig()).rejects.toThrow(/NEXT_PUBLIC_SITE_BASE_URL/); + }); + + it('supports manual mode when explicit overrides are provided', async () => { + clearEndpointOverrides(); + vi.stubEnv('NEXT_PUBLIC_SITE_BASE_URL', 'https://custom.modelseed.org'); + vi.stubEnv('NEXT_PUBLIC_API_BASE_URL', 'https://custom.modelseed.org/PMS'); + vi.stubEnv('NEXT_PUBLIC_REST_BASE_URL', 'https://custom.modelseed.org/api/v0'); + vi.stubEnv('NEXT_PUBLIC_STATUS_API_URL', 'https://custom.modelseed.org/api/test-service'); + vi.stubEnv('NEXT_PUBLIC_SOLR_BASE_URL', 'https://custom.modelseed.org/solr'); + vi.stubEnv('NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION', 'reactions_custom'); + vi.stubEnv('NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION', 'compounds_custom'); + const config = await loadConfig(); + expect(config.DEPLOYMENT_MODE).toBe(''); + expect(config.MODELSEED_API_URL).toBe('https://custom.modelseed.org/PMS'); + expect(config.SOLR_BASE).toBe('https://custom.modelseed.org/solr/'); + }); + + it('uses production defaults when NEXT_PUBLIC_DEPLOYMENT_MODE=production', async () => { + clearEndpointOverrides(); + vi.stubEnv('NEXT_PUBLIC_DEPLOYMENT_MODE', 'production'); + const config = await loadConfig(); + expect(config.DEPLOYMENT_MODE).toBe('production'); + expect(config.MODELSEED_API_URL).toBe('https://modelseed.org/PMS'); + expect(config.SOLR_REACTIONS_COLLECTION).toBe('reactions'); + expect(config.SOLR_COMPOUNDS_COLLECTION).toBe('compounds'); + }); + + it('keeps explicit API URL override as highest precedence', async () => { + vi.stubEnv('NEXT_PUBLIC_DEPLOYMENT_MODE', 'staging'); + vi.stubEnv('NEXT_PUBLIC_API_BASE_URL', 'https://modelseed.org/PMS/'); + const config = await loadConfig(); + expect(config.MODELSEED_API_URL).toBe('https://modelseed.org/PMS'); + }); + + it('uses explicit Solr collection names from override env', async () => { + vi.stubEnv('NEXT_PUBLIC_DEPLOYMENT_MODE', 'staging'); + vi.stubEnv('NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION', 'reactions_manual'); + vi.stubEnv('NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION', 'compounds_manual'); + const config = await loadConfig(); + expect(config.SOLR_REACTIONS_COLLECTION).toBe('reactions_manual'); + expect(config.SOLR_COMPOUNDS_COLLECTION).toBe('compounds_manual'); + }); + + it('uses mode-specific default env values when provided', async () => { + clearEndpointOverrides(); + vi.stubEnv('NEXT_PUBLIC_DEPLOYMENT_MODE', 'staging'); + vi.stubEnv('NEXT_PUBLIC_API_BASE_URL_STAGING', 'https://custom-staging.modelseed.org/PMS'); + vi.stubEnv('NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_STAGING', 'reactions_custom_staging'); + const config = await loadConfig(); + expect(config.MODELSEED_API_URL).toBe('https://custom-staging.modelseed.org/PMS'); + expect(config.SOLR_REACTIONS_COLLECTION).toBe('reactions_custom_staging'); + }); +}); diff --git a/tests/unit/api/modelseed-client.test.ts b/tests/unit/api/modelseed-client.test.ts index 58f9f552..aba4adbe 100644 --- a/tests/unit/api/modelseed-client.test.ts +++ b/tests/unit/api/modelseed-client.test.ts @@ -13,7 +13,7 @@ describe('modelseed API client wrappers', () => { vi.restoreAllMocks(); vi.resetModules(); vi.stubEnv('NEXT_PUBLIC_USE_MODELSEED_API', 'true'); - vi.stubEnv('NEXT_PUBLIC_MODELSEED_API_URL', 'http://localhost:8000'); + vi.stubEnv('NEXT_PUBLIC_API_BASE_URL', 'http://localhost:8000'); localStorage.clear(); setAuthToken(); }); diff --git a/tests/unit/api/reactionComments.test.ts b/tests/unit/api/reactionComments.test.ts new file mode 100644 index 00000000..e7f9f6d9 --- /dev/null +++ b/tests/unit/api/reactionComments.test.ts @@ -0,0 +1,73 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +describe('reaction comments API client', () => { + const mockFetch = vi.fn(); + vi.stubGlobal('fetch', mockFetch); + + beforeEach(() => { + vi.clearAllMocks(); + const store: Record = {}; + vi.stubGlobal('localStorage', { + getItem: (key: string) => store[key] || null, + setItem: (key: string, value: string) => { store[key] = value; }, + removeItem: (key: string) => { delete store[key]; }, + }); + }); + + it('posts reaction comment to the Next.js proxy with auth header when token exists', async () => { + localStorage.setItem('auth', JSON.stringify({ + user_id: 'alice@patricbrc.org', + token: 'test-token', + method: 'PATRIC', + })); + + mockFetch.mockResolvedValueOnce({ + ok: true, + text: () => Promise.resolve(JSON.stringify({ msg: 'ok' })), + }); + + const { submitReactionComment } = await import('@/lib/api/reactionComments'); + const response = await submitReactionComment({ + reactionId: 'rxn00001', + isAlias: true, + wrongStoichiometry: false, + remarks: 'Alias is wrong', + email: 'alice@example.org', + }); + + expect(response.message).toBe('ok'); + expect(mockFetch).toHaveBeenCalledWith( + '/api/biochem/comments', + expect.objectContaining({ + method: 'POST', + headers: expect.objectContaining({ + 'Content-Type': 'application/json', + 'X-ModelSEED-Auth': 'test-token', + }), + }), + ); + const body = JSON.parse(String(mockFetch.mock.calls[0]?.[1]?.body ?? '{}')) as Record; + expect(body.reactionId).toBe('rxn00001'); + expect(body.username).toBe('alice@patricbrc.org'); + expect(body.isAlias).toBe(true); + }); + + it('throws a helpful error when the proxy returns a failure', async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 400, + text: () => Promise.resolve(JSON.stringify({ message: 'bad payload' })), + }); + + const { submitReactionComment } = await import('@/lib/api/reactionComments'); + await expect( + submitReactionComment({ + reactionId: 'rxn00001', + isAlias: false, + wrongStoichiometry: false, + remarks: '', + email: '', + }), + ).rejects.toThrow('Failed to submit comment: bad payload'); + }); +}); diff --git a/tests/unit/utils/gridFiltering.test.ts b/tests/unit/utils/gridFiltering.test.ts index f23f61c5..c132cd58 100644 --- a/tests/unit/utils/gridFiltering.test.ts +++ b/tests/unit/utils/gridFiltering.test.ts @@ -1,10 +1,11 @@ import { describe, expect, it } from 'vitest'; import { GridFilterModel } from '@mui/x-data-grid'; -import { filterDocsByGridModel } from '@/lib/api/biochem'; import { filterRowsWithGridModel } from '@/lib/hooks/useToolbarGridFiltering'; describe('grid filtering helpers', () => { - it('supports OR logic for column filters', () => { + it('supports OR logic for column filters', async () => { + process.env.NEXT_PUBLIC_DEPLOYMENT_MODE = 'staging'; + const { filterDocsByGridModel } = await import('@/lib/api/biochem'); const rows = [ { id: 'm1', status: 'queued', type: 'model' }, { id: 'm2', status: 'completed', type: 'media' }, diff --git a/tests/utils/testHelpers.ts b/tests/utils/testHelpers.ts index e1a45c7f..00daa48e 100644 --- a/tests/utils/testHelpers.ts +++ b/tests/utils/testHelpers.ts @@ -51,7 +51,7 @@ export async function checkApiHealth(baseUrl: string): Promise { */ export const testConfig = { api: { - baseUrl: process.env.NEXT_PUBLIC_MODELSEED_API_URL || 'http://poplar.cels.anl.gov:8000', + baseUrl: process.env.NEXT_PUBLIC_API_BASE_URL || 'http://poplar.cels.anl.gov:8000', timeout: 30000, }, auth: { From 417019e4f1b9cd5f68fd4c091e67aeef37f8b9f2 Mon Sep 17 00:00:00 2001 From: VibhavSetlur Date: Mon, 11 May 2026 14:01:20 -0500 Subject: [PATCH 06/14] docs: update scripts and documentation for new env system - api-test.mjs: Use canonical env vars, normalized Solr URL handling - All test scripts: Updated to use new config exports - lib/api/README.md: Document config contract and precedence model - tests/README.md: Updated for new env system - docs/TESTING.md: Staging/production testing guide - docs/TROUBLESHOOTING.md: Env var troubleshooting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/TESTING-v3.0.0.md | 4 +- docs/TESTING.md | 2 +- docs/TROUBLESHOOTING.md | 6 +-- lib/api/README.md | 15 ++++-- scripts/api-test.mjs | 77 +++++++++++++++++++++++++---- scripts/check-jobs.mjs | 2 +- scripts/check_issues.mjs | 2 +- scripts/comprehensive-api-test.mjs | 2 +- scripts/diagnose-api.mjs | 2 +- scripts/test-api-simple.mjs | 2 +- scripts/test-fasta-upload.mjs | 2 +- scripts/test-model-copy.mjs | 2 +- scripts/test-reconstruct-upload.mjs | 2 +- scripts/test-reconstruct.mjs | 2 +- tests/README.md | 8 +-- tests/e2e/README.md | 2 +- 16 files changed, 97 insertions(+), 35 deletions(-) diff --git a/docs/TESTING-v3.0.0.md b/docs/TESTING-v3.0.0.md index 7d459ac5..95534376 100644 --- a/docs/TESTING-v3.0.0.md +++ b/docs/TESTING-v3.0.0.md @@ -9,7 +9,7 @@ Before testing, ensure you have: 2. ✅ SSH tunnel active: `ssh -L 8000:localhost:8000 user@poplar.cels.anl.gov` 3. ✅ Environment configured in `.env.local`: ```bash - NEXT_PUBLIC_MODELSEED_API_URL=http://localhost:8000 + NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 PATRIC_USERNAME=your_username PATRIC_PASSWORD=your_password ``` @@ -287,7 +287,7 @@ npm audit --omit=dev 3. **Test Environment**: - Use `.env.local` for configuration - - Ensure `NEXT_PUBLIC_MODELSEED_API_URL=http://localhost:8000` when using tunnel + - Ensure `NEXT_PUBLIC_API_BASE_URL=http://localhost:8000` when using tunnel - Keep SSH tunnel terminal open during testing 4. **Known Limitations**: diff --git a/docs/TESTING.md b/docs/TESTING.md index b87c6feb..e9ee9024 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -202,7 +202,7 @@ npm run test:e2e:ui ```yaml NEXT_PUBLIC_USE_MODELSEED_API: 'true' -NEXT_PUBLIC_MODELSEED_API_URL: 'http://poplar.cels.anl.gov:8000' +NEXT_PUBLIC_API_BASE_URL: 'http://poplar.cels.anl.gov:8000' PATRIC_TOKEN: ${{ secrets.PATRIC_TOKEN }} ``` diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 9709af42..97c06472 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -41,7 +41,7 @@ This guide covers common issues encountered during development and deployment of 4. **Check Environment Variable:** In `.env.local`: ```bash - NEXT_PUBLIC_MODELSEED_API_URL=http://localhost:8000 + NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 ``` 5. **Restart Dev Server:** After changing `.env.local`, restart: @@ -129,7 +129,7 @@ This guide covers common issues encountered during development and deployment of ```bash ssh -L 8001:localhost:8000 user@poplar.cels.anl.gov # Then update .env.local: - NEXT_PUBLIC_MODELSEED_API_URL=http://localhost:8001 + NEXT_PUBLIC_API_BASE_URL=http://localhost:8001 ``` --- @@ -495,7 +495,7 @@ npm run lint -- --fix # Auto-fix ```bash # .env.local -NEXT_PUBLIC_MODELSEED_API_URL=http://localhost:8000 +NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 NEXT_PUBLIC_USE_MODELSEED_API=true NEXT_PUBLIC_USE_NEW_PROXY=true PATRIC_USERNAME=your_username diff --git a/lib/api/README.md b/lib/api/README.md index 2ec939a8..ea07c212 100644 --- a/lib/api/README.md +++ b/lib/api/README.md @@ -96,7 +96,7 @@ if (current) { | `findReactionsForCompound(cpdId)` | Find reactions using a compound | | `getCompoundImageUrl(id)` | Get compound structure image URL | -**Routing**: Controlled by `USE_NEW_BIOCHEM` flag. Default is legacy Solr. +**Routing**: Reactions/compounds pages are Solr-backed (configured collections + base URL from `config.ts`). **Usage:** ```tsx @@ -120,18 +120,23 @@ const filtered = await getReactions({ | Constant | Default | Description | |----------|---------|-------------| -| `MODELSEED_API_URL` | `http://poplar.cels.anl.gov:8000` | ModelSEED REST API base | +| `DEPLOYMENT_MODE` | `staging` \| `production` \| `manual` (unset) | Deployment mode (`NEXT_PUBLIC_DEPLOYMENT_MODE`) | +| `MODELSEED_API_URL` | mode-derived (`https:///PMS`) | ModelSEED REST API base | | `USE_NEW_PROXY` | `true` | Route Workspace through REST proxy | | `USE_MODELSEED_API` | `true` | Use modelseed-api for user data | -| `USE_NEW_BIOCHEM` | `false` | Route biochemistry through REST | **Environment Variables:** | Variable | Controls | |----------|----------| -| `NEXT_PUBLIC_MODELSEED_API_URL` | Override API base URL | +| `NEXT_PUBLIC_DEPLOYMENT_MODE` | Endpoint profile (`staging`, `production`, or unset/manual with required overrides) | +| `NEXT_PUBLIC_API_BASE_URL` | Override API base URL | +| `NEXT_PUBLIC_API_BASE_URL_STAGING` / `NEXT_PUBLIC_API_BASE_URL_PRODUCTION` | Mode defaults for API base URL | +| `NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION` | Override Solr reactions core | +| `NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION` | Override Solr compounds core | +| `NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_STAGING` / `NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_PRODUCTION` | Mode defaults for reactions core | +| `NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_STAGING` / `NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_PRODUCTION` | Mode defaults for compounds core | | `NEXT_PUBLIC_USE_NEW_PROXY` | Toggle Workspace proxy | | `NEXT_PUBLIC_USE_MODELSEED_API` | Toggle modelseed-api | -| `NEXT_PUBLIC_USE_NEW_BIOCHEM` | Toggle biochem routing | ### jobTracker.ts diff --git a/scripts/api-test.mjs b/scripts/api-test.mjs index 401a0817..0385a076 100644 --- a/scripts/api-test.mjs +++ b/scripts/api-test.mjs @@ -13,7 +13,7 @@ * - SSH tunnel to API server: ssh -L 8000:localhost:8000 user@poplar.cels.anl.gov * * Environment Variables (.env.local): - * NEXT_PUBLIC_MODELSEED_API_URL=http://localhost:8000 + * NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 * PATRIC_USERNAME=your_username * PATRIC_PASSWORD=your_password */ @@ -22,12 +22,57 @@ import dotenv from 'dotenv'; dotenv.config({ path: '.env.local' }); dotenv.config({ path: '.env' }); -const API_URL = process.env.NEXT_PUBLIC_MODELSEED_API_URL || 'http://localhost:8000'; const USE_NEW_PROXY = process.env.NEXT_PUBLIC_USE_NEW_PROXY !== 'false'; +const DEPLOYMENT_MODE = (process.env.NEXT_PUBLIC_DEPLOYMENT_MODE || '').toLowerCase(); +const IS_MANUAL = DEPLOYMENT_MODE !== 'staging' && DEPLOYMENT_MODE !== 'production'; + +const SITE_BASE = process.env.NEXT_PUBLIC_SITE_BASE_URL + || ( + DEPLOYMENT_MODE === 'production' + ? (process.env.NEXT_PUBLIC_SITE_BASE_URL_PRODUCTION || 'https://modelseed.org') + : DEPLOYMENT_MODE === 'staging' + ? (process.env.NEXT_PUBLIC_SITE_BASE_URL_STAGING || 'https://staging.modelseed.org') + : '' + ); + +const API_URL = process.env.NEXT_PUBLIC_API_BASE_URL + || ( + DEPLOYMENT_MODE === 'production' + ? (process.env.NEXT_PUBLIC_API_BASE_URL_PRODUCTION || `${SITE_BASE}/PMS`) + : DEPLOYMENT_MODE === 'staging' + ? (process.env.NEXT_PUBLIC_API_BASE_URL_STAGING || `${SITE_BASE}/PMS`) + : '' + ); + const WORKSPACE_URL = USE_NEW_PROXY ? `${API_URL}/api/workspace` : 'https://p3.theseed.org/services/Workspace'; -const SOLR_BASE = process.env.NEXT_PUBLIC_USE_NEW_BIOCHEM === 'true' - ? `${API_URL}/api/solr/` - : 'https://modelseed.org/solr/'; + +const SOLR_BASE = process.env.NEXT_PUBLIC_SOLR_BASE_URL + || ( + DEPLOYMENT_MODE === 'production' + ? (process.env.NEXT_PUBLIC_SOLR_BASE_URL_PRODUCTION || `${SITE_BASE}/solr/`) + : DEPLOYMENT_MODE === 'staging' + ? (process.env.NEXT_PUBLIC_SOLR_BASE_URL_STAGING || `${SITE_BASE}/solr/`) + : '' + ); +const SOLR_BASE_NORMALIZED = SOLR_BASE.endsWith('/') ? SOLR_BASE : `${SOLR_BASE}/`; + +const REACTIONS_COLLECTION = process.env.NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION + || ( + DEPLOYMENT_MODE === 'production' + ? (process.env.NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_PRODUCTION || 'reactions') + : DEPLOYMENT_MODE === 'staging' + ? (process.env.NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_STAGING || 'reactions_staging') + : '' + ); + +const COMPOUNDS_COLLECTION = process.env.NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION + || ( + DEPLOYMENT_MODE === 'production' + ? (process.env.NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_PRODUCTION || 'compounds') + : DEPLOYMENT_MODE === 'staging' + ? (process.env.NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_STAGING || 'compounds_staging') + : '' + ); /* ============================================================================ * TEST UTILITIES @@ -157,7 +202,19 @@ async function testConfiguration() { test('Solr base URL is configured', async () => { assert(SOLR_BASE, 'SOLR_BASE not set'); - log(` Solr URL: ${SOLR_BASE}`, 'info'); + log(` Solr URL: ${SOLR_BASE_NORMALIZED}`, 'info'); + }); + + test('Solr collection names are set', async () => { + assert(REACTIONS_COLLECTION, 'Set NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION (e.g. reactions_staging or reactions)'); + assert(COMPOUNDS_COLLECTION, 'Set NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION (e.g. compounds_staging or compounds)'); + }); + + test('Manual mode has explicit endpoint overrides', async () => { + if (!IS_MANUAL) return; + assert(SITE_BASE, 'manual mode requires NEXT_PUBLIC_SITE_BASE_URL'); + assert(API_URL, 'manual mode requires NEXT_PUBLIC_API_BASE_URL'); + assert(SOLR_BASE, 'manual mode requires NEXT_PUBLIC_SOLR_BASE_URL'); }); test('API server is reachable', async () => { @@ -362,28 +419,28 @@ async function testBiochemApi() { logSection('Biochemistry API'); test('List reactions', async () => { - const result = await request(`${SOLR_BASE}reactions_staging/select?q=*:*&rows=10&wt=json`); + const result = await request(`${SOLR_BASE_NORMALIZED}${REACTIONS_COLLECTION}/select?q=*:*&rows=10&wt=json`); assert(result.response, 'No response in result'); assert(Array.isArray(result.response.docs), 'No docs in response'); log(` Found ${result.response.docs.length} reactions`, 'info'); }); test('List compounds', async () => { - const result = await request(`${SOLR_BASE}compounds_staging/select?q=*:*&rows=10&wt=json`); + const result = await request(`${SOLR_BASE_NORMALIZED}${COMPOUNDS_COLLECTION}/select?q=*:*&rows=10&wt=json`); assert(result.response, 'No response in result'); assert(Array.isArray(result.response.docs), 'No docs in response'); log(` Found ${result.response.docs.length} compounds`, 'info'); }); test('Get reaction by ID', async () => { - const result = await request(`${SOLR_BASE}reactions_staging/select?q=id:rxn00001&wt=json`); + const result = await request(`${SOLR_BASE_NORMALIZED}${REACTIONS_COLLECTION}/select?q=id:rxn00001&wt=json`); assert(result.response, 'No response'); assert(result.response.docs.length > 0, 'No reaction found'); log(` Got reaction: rxn00001`, 'success'); }); test('Get compound by ID', async () => { - const result = await request(`${SOLR_BASE}compounds_staging/select?q=id:cpd00001&wt=json`); + const result = await request(`${SOLR_BASE_NORMALIZED}${COMPOUNDS_COLLECTION}/select?q=id:cpd00001&wt=json`); assert(result.response, 'No response'); assert(result.response.docs.length > 0, 'No compound found'); log(` Got compound: cpd00001`, 'success'); diff --git a/scripts/check-jobs.mjs b/scripts/check-jobs.mjs index 8e8909af..bcbc9b0b 100644 --- a/scripts/check-jobs.mjs +++ b/scripts/check-jobs.mjs @@ -8,7 +8,7 @@ function fixToken(token) { } const token = fixToken(process.env.RAST_TOKEN); -const API_URL = process.env.NEXT_PUBLIC_MODELSEED_API_URL; +const API_URL = process.env.NEXT_PUBLIC_API_BASE_URL; const jobs = { 'e60bb1ce-9cfa-4ee3-b1df-4da671e655a4': 'Protein FASTA', diff --git a/scripts/check_issues.mjs b/scripts/check_issues.mjs index c6458900..415bac7a 100644 --- a/scripts/check_issues.mjs +++ b/scripts/check_issues.mjs @@ -1,7 +1,7 @@ import dotenv from 'dotenv'; dotenv.config({ path: '.env.local' }); -const API_URL = process.env.NEXT_PUBLIC_MODELSEED_API_URL || 'http://localhost:8000'; +const API_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'; const authToken = process.env.PATRIC_TOKEN || process.env.RAST_TOKEN || process.env.NEXT_PUBLIC_PATRIC_TOKEN; async function checkIssues() { diff --git a/scripts/comprehensive-api-test.mjs b/scripts/comprehensive-api-test.mjs index fc924530..8028a5b9 100755 --- a/scripts/comprehensive-api-test.mjs +++ b/scripts/comprehensive-api-test.mjs @@ -21,7 +21,7 @@ import dotenv from 'dotenv'; dotenv.config({ path: '.env.local' }); dotenv.config({ path: '.env' }); -const API_URL = process.env.NEXT_PUBLIC_MODELSEED_API_URL || 'http://localhost:8000'; +const API_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'; const USE_NEW_PROXY = process.env.NEXT_PUBLIC_USE_NEW_PROXY !== 'false'; // CLI args diff --git a/scripts/diagnose-api.mjs b/scripts/diagnose-api.mjs index 90cadae7..6876a563 100644 --- a/scripts/diagnose-api.mjs +++ b/scripts/diagnose-api.mjs @@ -7,7 +7,7 @@ import dotenv from 'dotenv'; dotenv.config({ path: '.env.local' }); -const API_URL = process.env.NEXT_PUBLIC_MODELSEED_API_URL || 'http://localhost:8000'; +const API_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'; const authToken = process.env.PATRIC_TOKEN || process.env.RAST_TOKEN; async function testEndpoint(endpoint, ref) { diff --git a/scripts/test-api-simple.mjs b/scripts/test-api-simple.mjs index a2588f4d..4aa77ead 100755 --- a/scripts/test-api-simple.mjs +++ b/scripts/test-api-simple.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node import 'dotenv/config'; -const API_URL = process.env.NEXT_PUBLIC_MODELSEED_API_URL || 'http://localhost:8000'; +const API_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'; const RAST_TOKEN = process.env.RAST_TOKEN; async function test(name, url, token) { diff --git a/scripts/test-fasta-upload.mjs b/scripts/test-fasta-upload.mjs index 6b6afe88..d9de6ded 100644 --- a/scripts/test-fasta-upload.mjs +++ b/scripts/test-fasta-upload.mjs @@ -9,7 +9,7 @@ import dotenv from 'dotenv'; dotenv.config({ path: '.env.local' }); -const API_URL = process.env.NEXT_PUBLIC_MODELSEED_API_URL || 'http://localhost:8000'; +const API_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'; function fixToken(token) { if (!token) return null; diff --git a/scripts/test-model-copy.mjs b/scripts/test-model-copy.mjs index 84f2e0f2..2ca38efb 100755 --- a/scripts/test-model-copy.mjs +++ b/scripts/test-model-copy.mjs @@ -2,7 +2,7 @@ import dotenv from 'dotenv'; dotenv.config({ path: '.env.local' }); -const API_URL = process.env.NEXT_PUBLIC_MODELSEED_API_URL || 'http://localhost:8000'; +const API_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'; const RAST_TOKEN = process.env.RAST_TOKEN; async function testModelCopy() { diff --git a/scripts/test-reconstruct-upload.mjs b/scripts/test-reconstruct-upload.mjs index 568ab1ec..e1b8dada 100644 --- a/scripts/test-reconstruct-upload.mjs +++ b/scripts/test-reconstruct-upload.mjs @@ -3,7 +3,7 @@ import dotenv from 'dotenv'; import fs from 'fs'; dotenv.config({ path: '.env.local' }); -const API_URL = process.env.NEXT_PUBLIC_MODELSEED_API_URL || 'http://localhost:8000'; +const API_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'; const authToken = process.env.PATRIC_TOKEN; async function testReconstructUpload() { diff --git a/scripts/test-reconstruct.mjs b/scripts/test-reconstruct.mjs index 123395e4..84e3ffc6 100644 --- a/scripts/test-reconstruct.mjs +++ b/scripts/test-reconstruct.mjs @@ -2,7 +2,7 @@ import dotenv from 'dotenv'; dotenv.config({ path: '.env.local' }); -const API_URL = process.env.NEXT_PUBLIC_MODELSEED_API_URL || 'http://localhost:8000'; +const API_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'; const authToken = process.env.PATRIC_TOKEN; async function testReconstruct() { diff --git a/tests/README.md b/tests/README.md index e2f18940..35145b2c 100644 --- a/tests/README.md +++ b/tests/README.md @@ -94,7 +94,7 @@ npx playwright test -g "public" ``` 3. **Environment variables** in `.env.local`: ```bash - NEXT_PUBLIC_MODELSEED_API_URL=http://localhost:8000 + NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 PATRIC_TOKEN=your_patric_token # For PATRIC workspace tests RAST_TOKEN=your_rast_token # For RAST workspace tests ``` @@ -136,7 +136,7 @@ npm run test:api ``` 2. **Environment variables** in `.env.local`: ```bash - NEXT_PUBLIC_MODELSEED_API_URL=http://localhost:8000 + NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 # PATRIC credentials (for /seaver@patricbrc.org/... paths) PATRIC_TOKEN=un=seaver@patricbrc.org|... @@ -199,7 +199,7 @@ The CI workflow (`.github/workflows/ci.yml`) runs on: | `RAST_USERNAME` | For API login | RAST username | | `RAST_PASSWORD` | For API login | RAST password | | `NEXT_PUBLIC_USE_MODELSEED_API` | No | Enable ModelSEED API (default: true) | -| `NEXT_PUBLIC_MODELSEED_API_URL` | No | API base URL | +| `NEXT_PUBLIC_API_BASE_URL` | No | API base URL | | `NEXT_PUBLIC_USE_NEW_PROXY` | No | Use new proxy (default: true) | ### PATRIC vs RAST Tokens @@ -219,7 +219,7 @@ RAST_TOKEN=un=seaver|... ### Example .env.local ```bash -NEXT_PUBLIC_MODELSEED_API_URL=http://localhost:8000 +NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 NEXT_PUBLIC_USE_MODELSEED_API=true # PATRIC credentials diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 2d66c6e8..e69c6fd2 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -24,7 +24,7 @@ Create a `.env.local` file with your PATRIC credentials: ```bash # .env.local -NEXT_PUBLIC_MODELSEED_API_URL=http://localhost:8000 +NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 NEXT_PUBLIC_USE_MODELSEED_API=true PATRIC_TOKEN=your_patric_token_here PATRIC_USERNAME=your_username From 9987ef18d87ac7245661688c7f28197aa6b1e554 Mon Sep 17 00:00:00 2001 From: VibhavSetlur Date: Mon, 11 May 2026 14:01:20 -0500 Subject: [PATCH 07/14] docs: update planning documents for env refactor completion - Phase 4: Comprehensive env system refactor and validation complete - Verification: All endpoints tested, all tests pass, build successful - Config resolution verified against staging and production services Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gsd/milestones/v1-alpha/3/4-PLAN.md | 2 +- .gsd/milestones/v1-alpha/3/VERIFICATION.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gsd/milestones/v1-alpha/3/4-PLAN.md b/.gsd/milestones/v1-alpha/3/4-PLAN.md index 65e12f3a..7ac42fc6 100644 --- a/.gsd/milestones/v1-alpha/3/4-PLAN.md +++ b/.gsd/milestones/v1-alpha/3/4-PLAN.md @@ -30,7 +30,7 @@ Rebuild the `/projects` and `/events` hubs, replicating the simple layout struct - Set up the grid blocks (`...
`) into MUI Grid/Flex components (using `Box` or `Stack` or equivalent inline CSS `display: flex`). - Build links (using `` vs `` where appropriate): - Internal: `href="/projects/fusions"`, `href="/projects/regulons"` - - External: `http://komodo.modelseed.org`, `http://minedatabase.mcs.anl.gov`, `http://coremodels.mcs.anl.gov`. + - External: `http://komodo.modelseed.org`, `https://minedatabase.mcs.anl.gov`, `http://coremodels.mcs.anl.gov`. - Fix missing image references `ms-projects/img/atomic-regulons.png` (can use placeholders if image doesn't exist locally, or verify we copied those in Phase 1). npm run check diff --git a/.gsd/milestones/v1-alpha/3/VERIFICATION.md b/.gsd/milestones/v1-alpha/3/VERIFICATION.md index 65e33bcf..9677b2af 100644 --- a/.gsd/milestones/v1-alpha/3/VERIFICATION.md +++ b/.gsd/milestones/v1-alpha/3/VERIFICATION.md @@ -56,7 +56,7 @@ File: lib/data/publications.ts (48317 bytes) 29: Link href="/projects/fusions" (internal) 42: href="http://komodo.modelseed.org" (external, target="_blank") 66: Link href="/projects/regulons" (internal) -87: href="http://minedatabase.mcs.anl.gov" (external, target="_blank") +87: href="https://minedatabase.mcs.anl.gov" (external, target="_blank") 103: href="http://coremodels.mcs.anl.gov" (external, target="_blank") ``` **Screenshot:** `phase3_projects_1772578681570.png` — Shows "ModelSEED Projects" heading, two-column grid with: From de7a319d4662e6dc5d57f5cf128efd777fa3e301 Mon Sep 17 00:00:00 2001 From: VibhavSetlur Date: Mon, 11 May 2026 14:11:08 -0500 Subject: [PATCH 08/14] fix(build): default deployment mode and harden deploy workflow - Default NEXT_PUBLIC_DEPLOYMENT_MODE to staging when unset - Add explicit manual mode for strict override validation - Fix CI build failure in /api/biochem/comments route collection phase - Restore profile-based docker compose staging/production services - Update deploy_container.sh for safe mode selection and profile builds - Correct production host-to-container port mapping (3001:3000) - Update docs and config tests for manual/unset semantics Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .env.example | 4 +- Dockerfile | 2 +- README.md | 2 +- deploy_container.sh | 75 +++++++++++++------ docker-compose.yml | 134 ++++++++++++++++++++-------------- lib/api/README.md | 4 +- lib/api/config.ts | 27 ++++--- tests/unit/api/config.test.ts | 12 ++- 8 files changed, 168 insertions(+), 92 deletions(-) diff --git a/.env.example b/.env.example index 58a73d6a..e82ac55f 100644 --- a/.env.example +++ b/.env.example @@ -2,8 +2,8 @@ # # 1) Copy to .env.local # 2) Choose deployment mode: -# - NEXT_PUBLIC_DEPLOYMENT_MODE=staging|production -# - or leave unset for strict manual mode (requires override vars below) +# - NEXT_PUBLIC_DEPLOYMENT_MODE=staging|production (recommended) +# - NEXT_PUBLIC_DEPLOYMENT_MODE=manual for strict override mode (requires vars below) # # Local Poplar tunnel example: # ssh -L 8000:localhost:8000 YOUR_USERNAME@poplar.cels.anl.gov diff --git a/Dockerfile b/Dockerfile index 35eb65c0..5cbb32ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN npm ci COPY . . # Tell Docker to expect these variables during the build -ARG NEXT_PUBLIC_DEPLOYMENT_MODE +ARG NEXT_PUBLIC_DEPLOYMENT_MODE=staging ARG NEXT_PUBLIC_SITE_BASE_URL ARG NEXT_PUBLIC_SITE_BASE_URL_STAGING ARG NEXT_PUBLIC_SITE_BASE_URL_PRODUCTION diff --git a/README.md b/README.md index 2f115982..64464864 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ ModelSEED-UI is the modern Next.js 16 + React 19 + MUI 7 interface for the Model Key configuration constants live in `lib/api/config.ts`: -- `DEPLOYMENT_MODE` – `NEXT_PUBLIC_DEPLOYMENT_MODE` (`staging|production`), or unset for strict manual override mode. +- `DEPLOYMENT_MODE` – `NEXT_PUBLIC_DEPLOYMENT_MODE` (`staging|production|manual`). Unset defaults to `staging`; use `manual` for strict override mode. - `MODELSEED_API_URL` – base URL for Poplar (currently `http://poplar.cels.anl.gov:8000` in development). - `USE_MODELSEED_API` – when `true`, user data flows (My Models, My Media, jobs) use `modelseed-api`. - `USE_NEW_PROXY` – when `true`, workspace calls route through the REST proxy at `${MODELSEED_API_URL}/api/workspace`. diff --git a/deploy_container.sh b/deploy_container.sh index a52f3123..d4a958db 100755 --- a/deploy_container.sh +++ b/deploy_container.sh @@ -1,36 +1,70 @@ #!/usr/bin/env bash +set -euo pipefail + +TARGET_MODE="${1:-${NEXT_PUBLIC_DEPLOYMENT_MODE:-}}" +AUTO_YES="${AUTO_YES:-false}" + +if [[ "${TARGET_MODE}" == "--yes" ]]; then + TARGET_MODE="" + AUTO_YES="true" +fi +if [[ "${2:-}" == "--yes" ]]; then + AUTO_YES="true" +fi # 1. Grab the manually set version from VERSION.md -# (Checks if the file exists, reads it, and strips any accidental whitespace/newlines) if [ -f "VERSION.md" ]; then - export NEXT_PUBLIC_GIT_VERSION=$(cat VERSION.md | xargs) + export NEXT_PUBLIC_GIT_VERSION + NEXT_PUBLIC_GIT_VERSION=$(xargs < VERSION.md) else echo "Warning: VERSION.md not found. Defaulting to 'unknown'." export NEXT_PUBLIC_GIT_VERSION="unknown" fi # 2. Get strictly the first 6 characters of the current commit hash -export NEXT_PUBLIC_GIT_COMMIT=$(git rev-parse HEAD 2>/dev/null | cut -c 1-6 || echo "unknown") +export NEXT_PUBLIC_GIT_COMMIT +NEXT_PUBLIC_GIT_COMMIT=$(git rev-parse HEAD 2>/dev/null | cut -c 1-6 || echo "unknown") # 3. Get the current Git branch -export NEXT_PUBLIC_GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") +export NEXT_PUBLIC_GIT_BRANCH +NEXT_PUBLIC_GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") -# 3.1 Resolve deployment mode unless caller explicitly sets it -if [ -z "${NEXT_PUBLIC_DEPLOYMENT_MODE}" ]; then +if [ -z "${TARGET_MODE}" ]; then case "${NEXT_PUBLIC_GIT_BRANCH}" in main|master|production) - export NEXT_PUBLIC_DEPLOYMENT_MODE="production" + TARGET_MODE="production" ;; *) - export NEXT_PUBLIC_DEPLOYMENT_MODE="staging" + TARGET_MODE="staging" + ;; + esac +fi + +if [ -t 0 ] && [ "${AUTO_YES}" != "true" ]; then + echo "Select deployment environment:" + echo " 1) Staging (profile: staging, host port 3000)" + echo " 2) Production (profile: production, host port 3001)" + read -r -p "Enter choice [1 or 2, default based on branch: ${TARGET_MODE}]: " env_choice + case "${env_choice}" in + 1) TARGET_MODE="staging" ;; + 2) TARGET_MODE="production" ;; + "") ;; + *) + echo "Invalid choice. Exiting." + exit 1 ;; esac fi -# 4. Set human-readable date (e.g., "May 1, 2026") -export NEXT_PUBLIC_DEPLOY_DATE=$(date +"%B %-d, %Y") +if [[ "${TARGET_MODE}" != "staging" && "${TARGET_MODE}" != "production" ]]; then + echo "Invalid deployment mode: ${TARGET_MODE}. Use staging or production." + exit 1 +fi + +export NEXT_PUBLIC_DEPLOYMENT_MODE="${TARGET_MODE}" +export NEXT_PUBLIC_DEPLOY_DATE +NEXT_PUBLIC_DEPLOY_DATE=$(date +"%B %-d, %Y") -# Display the gathered metadata echo "========================================" echo "Ready to build ModelSEED UI:" echo " Version: $NEXT_PUBLIC_GIT_VERSION" @@ -41,14 +75,13 @@ echo " Date: $NEXT_PUBLIC_DEPLOY_DATE" echo "========================================" echo "" -# Ask for confirmation -read -p "Trigger Build? [y/N]: " confirm - -# Check the user's input -if [[ "$confirm" =~ ^[Yy]$ ]]; then - echo "Starting build process..." - docker compose up -d --build -else - echo "Build aborted. No changes were made." - exit 0 +if [ -t 0 ] && [ "${AUTO_YES}" != "true" ]; then + read -r -p "Trigger Build? [y/N]: " confirm + if [[ ! "$confirm" =~ ^[Yy]$ ]]; then + echo "Build aborted. No changes were made." + exit 0 + fi fi + +echo "Starting build process for ${NEXT_PUBLIC_DEPLOYMENT_MODE}..." +docker compose --profile "${NEXT_PUBLIC_DEPLOYMENT_MODE}" up -d --build diff --git a/docker-compose.yml b/docker-compose.yml index e08d3c92..227d8e2a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,63 +1,87 @@ +x-app-build-args: &app_build_args + NEXT_PUBLIC_GIT_VERSION: ${NEXT_PUBLIC_GIT_VERSION:-} + NEXT_PUBLIC_GIT_COMMIT: ${NEXT_PUBLIC_GIT_COMMIT:-} + NEXT_PUBLIC_GIT_BRANCH: ${NEXT_PUBLIC_GIT_BRANCH:-} + NEXT_PUBLIC_DEPLOY_DATE: ${NEXT_PUBLIC_DEPLOY_DATE:-} + NEXT_PUBLIC_SITE_BASE_URL: ${NEXT_PUBLIC_SITE_BASE_URL:-} + NEXT_PUBLIC_SITE_BASE_URL_STAGING: ${NEXT_PUBLIC_SITE_BASE_URL_STAGING:-} + NEXT_PUBLIC_SITE_BASE_URL_PRODUCTION: ${NEXT_PUBLIC_SITE_BASE_URL_PRODUCTION:-} + NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL:-} + NEXT_PUBLIC_API_BASE_URL_STAGING: ${NEXT_PUBLIC_API_BASE_URL_STAGING:-} + NEXT_PUBLIC_API_BASE_URL_PRODUCTION: ${NEXT_PUBLIC_API_BASE_URL_PRODUCTION:-} + NEXT_PUBLIC_REST_BASE_URL: ${NEXT_PUBLIC_REST_BASE_URL:-} + NEXT_PUBLIC_REST_BASE_URL_STAGING: ${NEXT_PUBLIC_REST_BASE_URL_STAGING:-} + NEXT_PUBLIC_REST_BASE_URL_PRODUCTION: ${NEXT_PUBLIC_REST_BASE_URL_PRODUCTION:-} + NEXT_PUBLIC_SOLR_BASE_URL: ${NEXT_PUBLIC_SOLR_BASE_URL:-} + NEXT_PUBLIC_SOLR_BASE_URL_STAGING: ${NEXT_PUBLIC_SOLR_BASE_URL_STAGING:-} + NEXT_PUBLIC_SOLR_BASE_URL_PRODUCTION: ${NEXT_PUBLIC_SOLR_BASE_URL_PRODUCTION:-} + NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION: ${NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION:-} + NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_STAGING: ${NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_STAGING:-} + NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_PRODUCTION: ${NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_PRODUCTION:-} + NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION: ${NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION:-} + NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_STAGING: ${NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_STAGING:-} + NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_PRODUCTION: ${NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_PRODUCTION:-} + NEXT_PUBLIC_STATUS_API_URL: ${NEXT_PUBLIC_STATUS_API_URL:-} + NEXT_PUBLIC_STATUS_API_URL_STAGING: ${NEXT_PUBLIC_STATUS_API_URL_STAGING:-} + NEXT_PUBLIC_STATUS_API_URL_PRODUCTION: ${NEXT_PUBLIC_STATUS_API_URL_PRODUCTION:-} + NEXT_PUBLIC_USE_MODELSEED_API: ${NEXT_PUBLIC_USE_MODELSEED_API:-true} + NEXT_PUBLIC_USE_NEW_PROXY: ${NEXT_PUBLIC_USE_NEW_PROXY:-true} + +x-app-runtime-env: &app_runtime_env + NODE_ENV: production + NEXT_PUBLIC_SITE_BASE_URL: ${NEXT_PUBLIC_SITE_BASE_URL:-} + NEXT_PUBLIC_SITE_BASE_URL_STAGING: ${NEXT_PUBLIC_SITE_BASE_URL_STAGING:-} + NEXT_PUBLIC_SITE_BASE_URL_PRODUCTION: ${NEXT_PUBLIC_SITE_BASE_URL_PRODUCTION:-} + NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL:-} + NEXT_PUBLIC_API_BASE_URL_STAGING: ${NEXT_PUBLIC_API_BASE_URL_STAGING:-} + NEXT_PUBLIC_API_BASE_URL_PRODUCTION: ${NEXT_PUBLIC_API_BASE_URL_PRODUCTION:-} + NEXT_PUBLIC_REST_BASE_URL: ${NEXT_PUBLIC_REST_BASE_URL:-} + NEXT_PUBLIC_REST_BASE_URL_STAGING: ${NEXT_PUBLIC_REST_BASE_URL_STAGING:-} + NEXT_PUBLIC_REST_BASE_URL_PRODUCTION: ${NEXT_PUBLIC_REST_BASE_URL_PRODUCTION:-} + NEXT_PUBLIC_SOLR_BASE_URL: ${NEXT_PUBLIC_SOLR_BASE_URL:-} + NEXT_PUBLIC_SOLR_BASE_URL_STAGING: ${NEXT_PUBLIC_SOLR_BASE_URL_STAGING:-} + NEXT_PUBLIC_SOLR_BASE_URL_PRODUCTION: ${NEXT_PUBLIC_SOLR_BASE_URL_PRODUCTION:-} + NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION: ${NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION:-} + NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_STAGING: ${NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_STAGING:-} + NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_PRODUCTION: ${NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_PRODUCTION:-} + NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION: ${NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION:-} + NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_STAGING: ${NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_STAGING:-} + NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_PRODUCTION: ${NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_PRODUCTION:-} + NEXT_PUBLIC_STATUS_API_URL: ${NEXT_PUBLIC_STATUS_API_URL:-} + NEXT_PUBLIC_STATUS_API_URL_STAGING: ${NEXT_PUBLIC_STATUS_API_URL_STAGING:-} + NEXT_PUBLIC_STATUS_API_URL_PRODUCTION: ${NEXT_PUBLIC_STATUS_API_URL_PRODUCTION:-} + NEXT_PUBLIC_USE_MODELSEED_API: ${NEXT_PUBLIC_USE_MODELSEED_API:-true} + NEXT_PUBLIC_USE_NEW_PROXY: ${NEXT_PUBLIC_USE_NEW_PROXY:-true} + services: - modelseed-ui: + modelseed-ui-staging: build: context: . args: - - NEXT_PUBLIC_GIT_VERSION=${NEXT_PUBLIC_GIT_VERSION} - - NEXT_PUBLIC_GIT_COMMIT=${NEXT_PUBLIC_GIT_COMMIT} - - NEXT_PUBLIC_GIT_BRANCH=${NEXT_PUBLIC_GIT_BRANCH} - - NEXT_PUBLIC_DEPLOY_DATE=${NEXT_PUBLIC_DEPLOY_DATE} - - NEXT_PUBLIC_DEPLOYMENT_MODE=${NEXT_PUBLIC_DEPLOYMENT_MODE:-staging} - - NEXT_PUBLIC_SITE_BASE_URL=${NEXT_PUBLIC_SITE_BASE_URL:-} - - NEXT_PUBLIC_SITE_BASE_URL_STAGING=${NEXT_PUBLIC_SITE_BASE_URL_STAGING:-} - - NEXT_PUBLIC_SITE_BASE_URL_PRODUCTION=${NEXT_PUBLIC_SITE_BASE_URL_PRODUCTION:-} - - NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL:-} - - NEXT_PUBLIC_API_BASE_URL_STAGING=${NEXT_PUBLIC_API_BASE_URL_STAGING:-} - - NEXT_PUBLIC_API_BASE_URL_PRODUCTION=${NEXT_PUBLIC_API_BASE_URL_PRODUCTION:-} - - NEXT_PUBLIC_REST_BASE_URL=${NEXT_PUBLIC_REST_BASE_URL:-} - - NEXT_PUBLIC_REST_BASE_URL_STAGING=${NEXT_PUBLIC_REST_BASE_URL_STAGING:-} - - NEXT_PUBLIC_REST_BASE_URL_PRODUCTION=${NEXT_PUBLIC_REST_BASE_URL_PRODUCTION:-} - - NEXT_PUBLIC_SOLR_BASE_URL=${NEXT_PUBLIC_SOLR_BASE_URL:-} - - NEXT_PUBLIC_SOLR_BASE_URL_STAGING=${NEXT_PUBLIC_SOLR_BASE_URL_STAGING:-} - - NEXT_PUBLIC_SOLR_BASE_URL_PRODUCTION=${NEXT_PUBLIC_SOLR_BASE_URL_PRODUCTION:-} - - NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION=${NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION:-} - - NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_STAGING=${NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_STAGING:-} - - NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_PRODUCTION=${NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_PRODUCTION:-} - - NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION=${NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION:-} - - NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_STAGING=${NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_STAGING:-} - - NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_PRODUCTION=${NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_PRODUCTION:-} - - NEXT_PUBLIC_STATUS_API_URL=${NEXT_PUBLIC_STATUS_API_URL:-} - - NEXT_PUBLIC_STATUS_API_URL_STAGING=${NEXT_PUBLIC_STATUS_API_URL_STAGING:-} - - NEXT_PUBLIC_STATUS_API_URL_PRODUCTION=${NEXT_PUBLIC_STATUS_API_URL_PRODUCTION:-} - - NEXT_PUBLIC_USE_MODELSEED_API=true - - NEXT_PUBLIC_USE_NEW_PROXY=true - container_name: modelseed_ui + <<: *app_build_args + NEXT_PUBLIC_DEPLOYMENT_MODE: staging + container_name: modelseed_ui_staging ports: - "3000:3000" restart: unless-stopped environment: - - NODE_ENV=production - - NEXT_PUBLIC_DEPLOYMENT_MODE=${NEXT_PUBLIC_DEPLOYMENT_MODE:-staging} - - NEXT_PUBLIC_SITE_BASE_URL=${NEXT_PUBLIC_SITE_BASE_URL:-} - - NEXT_PUBLIC_SITE_BASE_URL_STAGING=${NEXT_PUBLIC_SITE_BASE_URL_STAGING:-} - - NEXT_PUBLIC_SITE_BASE_URL_PRODUCTION=${NEXT_PUBLIC_SITE_BASE_URL_PRODUCTION:-} - - NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL:-} - - NEXT_PUBLIC_API_BASE_URL_STAGING=${NEXT_PUBLIC_API_BASE_URL_STAGING:-} - - NEXT_PUBLIC_API_BASE_URL_PRODUCTION=${NEXT_PUBLIC_API_BASE_URL_PRODUCTION:-} - - NEXT_PUBLIC_REST_BASE_URL=${NEXT_PUBLIC_REST_BASE_URL:-} - - NEXT_PUBLIC_REST_BASE_URL_STAGING=${NEXT_PUBLIC_REST_BASE_URL_STAGING:-} - - NEXT_PUBLIC_REST_BASE_URL_PRODUCTION=${NEXT_PUBLIC_REST_BASE_URL_PRODUCTION:-} - - NEXT_PUBLIC_SOLR_BASE_URL=${NEXT_PUBLIC_SOLR_BASE_URL:-} - - NEXT_PUBLIC_SOLR_BASE_URL_STAGING=${NEXT_PUBLIC_SOLR_BASE_URL_STAGING:-} - - NEXT_PUBLIC_SOLR_BASE_URL_PRODUCTION=${NEXT_PUBLIC_SOLR_BASE_URL_PRODUCTION:-} - - NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION=${NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION:-} - - NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_STAGING=${NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_STAGING:-} - - NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_PRODUCTION=${NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_PRODUCTION:-} - - NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION=${NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION:-} - - NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_STAGING=${NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_STAGING:-} - - NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_PRODUCTION=${NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_PRODUCTION:-} - - NEXT_PUBLIC_STATUS_API_URL=${NEXT_PUBLIC_STATUS_API_URL:-} - - NEXT_PUBLIC_STATUS_API_URL_STAGING=${NEXT_PUBLIC_STATUS_API_URL_STAGING:-} - - NEXT_PUBLIC_STATUS_API_URL_PRODUCTION=${NEXT_PUBLIC_STATUS_API_URL_PRODUCTION:-} - - NEXT_PUBLIC_USE_MODELSEED_API=true - - NEXT_PUBLIC_USE_NEW_PROXY=true + <<: *app_runtime_env + NEXT_PUBLIC_DEPLOYMENT_MODE: staging + profiles: + - staging + + modelseed-ui-production: + build: + context: . + args: + <<: *app_build_args + NEXT_PUBLIC_DEPLOYMENT_MODE: production + container_name: modelseed_ui_production + ports: + - "3001:3000" + restart: unless-stopped + environment: + <<: *app_runtime_env + NEXT_PUBLIC_DEPLOYMENT_MODE: production + profiles: + - production diff --git a/lib/api/README.md b/lib/api/README.md index ea07c212..f6420765 100644 --- a/lib/api/README.md +++ b/lib/api/README.md @@ -120,7 +120,7 @@ const filtered = await getReactions({ | Constant | Default | Description | |----------|---------|-------------| -| `DEPLOYMENT_MODE` | `staging` \| `production` \| `manual` (unset) | Deployment mode (`NEXT_PUBLIC_DEPLOYMENT_MODE`) | +| `DEPLOYMENT_MODE` | `staging` \| `production` \| `manual` | Deployment mode (`NEXT_PUBLIC_DEPLOYMENT_MODE`, defaults to `staging` when unset) | | `MODELSEED_API_URL` | mode-derived (`https:///PMS`) | ModelSEED REST API base | | `USE_NEW_PROXY` | `true` | Route Workspace through REST proxy | | `USE_MODELSEED_API` | `true` | Use modelseed-api for user data | @@ -128,7 +128,7 @@ const filtered = await getReactions({ **Environment Variables:** | Variable | Controls | |----------|----------| -| `NEXT_PUBLIC_DEPLOYMENT_MODE` | Endpoint profile (`staging`, `production`, or unset/manual with required overrides) | +| `NEXT_PUBLIC_DEPLOYMENT_MODE` | Endpoint profile (`staging`, `production`, or `manual`; unset defaults to `staging`) | | `NEXT_PUBLIC_API_BASE_URL` | Override API base URL | | `NEXT_PUBLIC_API_BASE_URL_STAGING` / `NEXT_PUBLIC_API_BASE_URL_PRODUCTION` | Mode defaults for API base URL | | `NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION` | Override Solr reactions core | diff --git a/lib/api/config.ts b/lib/api/config.ts index 629bfc17..dbe92773 100644 --- a/lib/api/config.ts +++ b/lib/api/config.ts @@ -25,19 +25,27 @@ function ensureTrailingSlash(value: string): string { const DEPLOYMENT_MODE_VAR = 'NEXT_PUBLIC_DEPLOYMENT_MODE'; -export type DeploymentMode = 'staging' | 'production' | ''; +export type DeploymentMode = 'staging' | 'production' | 'manual'; function resolveDeploymentMode(raw: string | undefined): DeploymentMode { const normalized = raw?.trim().toLowerCase(); if (normalized === 'staging' || normalized === 'production') { return normalized; } - return ''; + if (normalized === 'manual') { + return normalized; + } + if (!normalized) { + return 'staging'; + } + throw new Error( + `Invalid ${DEPLOYMENT_MODE_VAR} value "${raw}". Use staging, production, or manual.`, + ); } function throwManualModeError(overrideVar: string, description: string): never { throw new Error( - `Missing required environment variable ${overrideVar} while ${DEPLOYMENT_MODE_VAR} is unset. ` + + `Missing required environment variable ${overrideVar} while ${DEPLOYMENT_MODE_VAR}=manual. ` + `Set ${overrideVar} (${description}) or set ${DEPLOYMENT_MODE_VAR}=staging|production.`, ); } @@ -53,7 +61,7 @@ function resolveModeValue(params: { const overrideValue = toNonEmpty(readEnv(params.overrideVar)); if (overrideValue) return overrideValue; - if (!DEPLOYMENT_MODE) { + if (DEPLOYMENT_MODE === 'manual') { return throwManualModeError(params.overrideVar, params.manualDescription); } @@ -76,8 +84,9 @@ const SITE_DEFAULTS = { /** * Canonical deployment mode selector. - * - staging | production - * - unset/invalid => strict manual mode (explicit overrides required) + * - staging | production | manual + * - unset => staging (build-safe default) + * - manual => strict override mode (explicit endpoint vars required) */ export const DEPLOYMENT_MODE = resolveDeploymentMode(readEnv(DEPLOYMENT_MODE_VAR)); @@ -85,9 +94,9 @@ export const DEPLOYMENT_MODE = resolveDeploymentMode(readEnv(DEPLOYMENT_MODE_VAR * Backward-compatible constant name for existing imports. * This is not an env var alias. */ -export const DEPLOY_ENV = DEPLOYMENT_MODE; +export const DEPLOY_ENV = DEPLOYMENT_MODE === 'manual' ? '' : DEPLOYMENT_MODE; -export const DEPLOY_ENV_LABEL = DEPLOYMENT_MODE || 'manual'; +export const DEPLOY_ENV_LABEL = DEPLOYMENT_MODE; /** * Base ModelSEED site host (no trailing slash). @@ -204,7 +213,7 @@ function resolveSolrCollection(params: { const overrideValue = toNonEmpty(readEnv(params.overrideVar)); if (overrideValue) return overrideValue; - if (!DEPLOYMENT_MODE) { + if (DEPLOYMENT_MODE === 'manual') { return throwManualModeError(params.overrideVar, params.description); } diff --git a/tests/unit/api/config.test.ts b/tests/unit/api/config.test.ts index ae522365..7793fa1e 100644 --- a/tests/unit/api/config.test.ts +++ b/tests/unit/api/config.test.ts @@ -24,11 +24,13 @@ describe('api config deployment resolution', () => { it('throws in manual mode when required override vars are missing', async () => { clearEndpointOverrides(); + vi.stubEnv('NEXT_PUBLIC_DEPLOYMENT_MODE', 'manual'); await expect(loadConfig()).rejects.toThrow(/NEXT_PUBLIC_SITE_BASE_URL/); }); it('supports manual mode when explicit overrides are provided', async () => { clearEndpointOverrides(); + vi.stubEnv('NEXT_PUBLIC_DEPLOYMENT_MODE', 'manual'); vi.stubEnv('NEXT_PUBLIC_SITE_BASE_URL', 'https://custom.modelseed.org'); vi.stubEnv('NEXT_PUBLIC_API_BASE_URL', 'https://custom.modelseed.org/PMS'); vi.stubEnv('NEXT_PUBLIC_REST_BASE_URL', 'https://custom.modelseed.org/api/v0'); @@ -37,11 +39,19 @@ describe('api config deployment resolution', () => { vi.stubEnv('NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION', 'reactions_custom'); vi.stubEnv('NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION', 'compounds_custom'); const config = await loadConfig(); - expect(config.DEPLOYMENT_MODE).toBe(''); + expect(config.DEPLOYMENT_MODE).toBe('manual'); expect(config.MODELSEED_API_URL).toBe('https://custom.modelseed.org/PMS'); expect(config.SOLR_BASE).toBe('https://custom.modelseed.org/solr/'); }); + it('defaults to staging mode when NEXT_PUBLIC_DEPLOYMENT_MODE is unset', async () => { + clearEndpointOverrides(); + const config = await loadConfig(); + expect(config.DEPLOYMENT_MODE).toBe('staging'); + expect(config.MODELSEED_SITE_BASE_URL).toBe('https://staging.modelseed.org'); + expect(config.SOLR_REACTIONS_COLLECTION).toBe('reactions_staging'); + }); + it('uses production defaults when NEXT_PUBLIC_DEPLOYMENT_MODE=production', async () => { clearEndpointOverrides(); vi.stubEnv('NEXT_PUBLIC_DEPLOYMENT_MODE', 'production'); From 3abcfa19530a3ac7c7e8e511d341cf00814006d4 Mon Sep 17 00:00:00 2001 From: VibhavSetlur Date: Mon, 11 May 2026 14:21:04 -0500 Subject: [PATCH 09/14] chore(security): upgrade next.js to patched 16.2.6 release - Bump next to ^16.2.6 to clear high-severity advisories - Align eslint-config-next to ^16.2.6 - Update lockfile and verify npm audit --omit=dev --audit-level=high passes - Keep existing app behavior and build outputs unchanged Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- package-lock.json | 108 +++++++++++++++++++++++++--------------------- package.json | 4 +- 2 files changed, 62 insertions(+), 50 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1947a60d..43927759 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@mui/x-data-grid": "^8.27.3", "@rdkit/rdkit": "2025.3.4-1.0.0", "@tanstack/react-query": "^5.90.21", - "next": "^16.2.4", + "next": "^16.2.6", "react": "19.2.3", "react-dom": "19.2.3", "react-markdown": "^10.1.0", @@ -34,7 +34,7 @@ "@vitest/coverage-v8": "^4.1.1", "dotenv": "^17.3.1", "eslint": "^9", - "eslint-config-next": "16.2.1", + "eslint-config-next": "^16.2.6", "happy-dom": "^20.8.4", "tsx": "^4.21.0", "typescript": "^5", @@ -2046,15 +2046,15 @@ } }, "node_modules/@next/env": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.4.tgz", - "integrity": "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz", + "integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.1.tgz", - "integrity": "sha512-r0epZGo24eT4g08jJlg2OEryBphXqO8aL18oajoTKLzHJ6jVr6P6FI58DLMug04MwD3j8Fj0YK0slyzneKVyzA==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.6.tgz", + "integrity": "sha512-Z8l6o4JWKUl755x4R+wogD86KPeU+Ckw4K+SYG4kHeOJtRenDeK+OSbGcqZpDtbwn9DsJVdir2UxmwXuinUbUw==", "dev": true, "license": "MIT", "dependencies": { @@ -2062,9 +2062,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.4.tgz", - "integrity": "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz", + "integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==", "cpu": [ "arm64" ], @@ -2078,9 +2078,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.4.tgz", - "integrity": "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz", + "integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==", "cpu": [ "x64" ], @@ -2094,12 +2094,15 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.4.tgz", - "integrity": "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz", + "integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2110,12 +2113,15 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.4.tgz", - "integrity": "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz", + "integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2126,12 +2132,15 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.4.tgz", - "integrity": "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz", + "integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2142,12 +2151,15 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.4.tgz", - "integrity": "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz", + "integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2158,9 +2170,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.4.tgz", - "integrity": "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz", + "integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==", "cpu": [ "arm64" ], @@ -2174,9 +2186,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.4.tgz", - "integrity": "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz", + "integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==", "cpu": [ "x64" ], @@ -4910,13 +4922,13 @@ } }, "node_modules/eslint-config-next": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.1.tgz", - "integrity": "sha512-qhabwjQZ1Mk53XzXvmogf8KQ0tG0CQXF0CZ56+2/lVhmObgmaqj7x5A1DSrWdZd3kwI7GTPGUjFne+krRxYmFg==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.6.tgz", + "integrity": "sha512-z2ELYSkyrrJ6cuunTU8vhsT/RpouPkjaSah06nVW6Rg2Hpg0Vs8s497/e5s8G8qtdp4ccsiovz5P1rv+5VSW2Q==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "16.2.1", + "@next/eslint-plugin-next": "16.2.6", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", @@ -7751,12 +7763,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.4.tgz", - "integrity": "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", + "integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==", "license": "MIT", "dependencies": { - "@next/env": "16.2.4", + "@next/env": "16.2.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -7770,14 +7782,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.4", - "@next/swc-darwin-x64": "16.2.4", - "@next/swc-linux-arm64-gnu": "16.2.4", - "@next/swc-linux-arm64-musl": "16.2.4", - "@next/swc-linux-x64-gnu": "16.2.4", - "@next/swc-linux-x64-musl": "16.2.4", - "@next/swc-win32-arm64-msvc": "16.2.4", - "@next/swc-win32-x64-msvc": "16.2.4", + "@next/swc-darwin-arm64": "16.2.6", + "@next/swc-darwin-x64": "16.2.6", + "@next/swc-linux-arm64-gnu": "16.2.6", + "@next/swc-linux-arm64-musl": "16.2.6", + "@next/swc-linux-x64-gnu": "16.2.6", + "@next/swc-linux-x64-musl": "16.2.6", + "@next/swc-win32-arm64-msvc": "16.2.6", + "@next/swc-win32-x64-msvc": "16.2.6", "sharp": "^0.34.5" }, "peerDependencies": { diff --git a/package.json b/package.json index b3dedd0f..1966ca0b 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "@mui/x-data-grid": "^8.27.3", "@rdkit/rdkit": "2025.3.4-1.0.0", "@tanstack/react-query": "^5.90.21", - "next": "^16.2.4", + "next": "^16.2.6", "react": "19.2.3", "react-dom": "19.2.3", "react-markdown": "^10.1.0", @@ -46,7 +46,7 @@ "@vitest/coverage-v8": "^4.1.1", "dotenv": "^17.3.1", "eslint": "^9", - "eslint-config-next": "16.2.1", + "eslint-config-next": "^16.2.6", "happy-dom": "^20.8.4", "tsx": "^4.21.0", "typescript": "^5", From 83a2faaf3c25fb94b288d6a08c5239a95baa613d Mon Sep 17 00:00:00 2001 From: VibhavSetlur Date: Mon, 11 May 2026 14:31:29 -0500 Subject: [PATCH 10/14] fix: resolve Copilot review issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix readEnv() to use static env access for proper Next.js inlining - Add readEnvSafe() for dynamic key lookups while maintaining static map - Add NEXT_PUBLIC_PROBMODELSEED_URL to env docs and Docker files - Fix JSON parse error handling in comments route (return 400 on invalid) - Fix boolean parsing in comments route (use typeof check) - Fix test env pollution in gridFiltering.test.ts (use vi.stubEnv) Verified: lint ✅, tests ✅, build ✅, docker compose ✅ --- .env.example | 7 ++++ Dockerfile | 1 + app/api/biochem/comments/route.ts | 12 ++++-- docker-compose.yml | 2 + lib/api/config.ts | 54 +++++++++++++++++++++++--- tests/unit/utils/gridFiltering.test.ts | 11 +++++- 6 files changed, 76 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index e82ac55f..1ba0d436 100644 --- a/.env.example +++ b/.env.example @@ -73,6 +73,13 @@ NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_PRODUCTION=compounds NEXT_PUBLIC_USE_MODELSEED_API=true NEXT_PUBLIC_USE_NEW_PROXY=true +# ========================= +# ProbModelSEED URL Override +# ========================= +# Override for ProbModelSEED API endpoint (no trailing slash) +# Defaults to {SITE_BASE}/api/model when USE_NEW_PROXY=true +NEXT_PUBLIC_PROBMODELSEED_URL= + # ========================= # Build Metadata (/about/version) # ========================= diff --git a/Dockerfile b/Dockerfile index 5cbb32ed..33b4de72 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,6 +36,7 @@ ARG NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_STAGING ARG NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_PRODUCTION ARG NEXT_PUBLIC_USE_MODELSEED_API ARG NEXT_PUBLIC_USE_NEW_PROXY +ARG NEXT_PUBLIC_PROBMODELSEED_URL ARG NEXT_PUBLIC_GIT_VERSION ARG NEXT_PUBLIC_GIT_COMMIT ARG NEXT_PUBLIC_GIT_BRANCH diff --git a/app/api/biochem/comments/route.ts b/app/api/biochem/comments/route.ts index da2670da..030bc486 100644 --- a/app/api/biochem/comments/route.ts +++ b/app/api/biochem/comments/route.ts @@ -11,13 +11,19 @@ interface CommentRequestBody { } export async function POST(request: NextRequest) { - const body = (await request.json()) as CommentRequestBody; + let body: CommentRequestBody; + try { + body = (await request.json()) as CommentRequestBody; + } catch { + return NextResponse.json({ message: 'Invalid JSON body' }, { status: 400 }); + } + const reactionId = typeof body.reactionId === 'string' ? body.reactionId.trim() : ''; const remarks = typeof body.remarks === 'string' ? body.remarks.trim() : ''; const email = typeof body.email === 'string' ? body.email.trim() : ''; const username = typeof body.username === 'string' ? body.username.trim() : ''; - const isAlias = Boolean(body.isAlias); - const wrongStoichiometry = Boolean(body.wrongStoichiometry); + const isAlias = typeof body.isAlias === 'boolean' ? body.isAlias : false; + const wrongStoichiometry = typeof body.wrongStoichiometry === 'boolean' ? body.wrongStoichiometry : false; if (!reactionId) { return NextResponse.json({ message: 'reactionId is required' }, { status: 400 }); diff --git a/docker-compose.yml b/docker-compose.yml index 227d8e2a..f41fe7a9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,6 +26,7 @@ x-app-build-args: &app_build_args NEXT_PUBLIC_STATUS_API_URL_PRODUCTION: ${NEXT_PUBLIC_STATUS_API_URL_PRODUCTION:-} NEXT_PUBLIC_USE_MODELSEED_API: ${NEXT_PUBLIC_USE_MODELSEED_API:-true} NEXT_PUBLIC_USE_NEW_PROXY: ${NEXT_PUBLIC_USE_NEW_PROXY:-true} + NEXT_PUBLIC_PROBMODELSEED_URL: ${NEXT_PUBLIC_PROBMODELSEED_URL:-} x-app-runtime-env: &app_runtime_env NODE_ENV: production @@ -52,6 +53,7 @@ x-app-runtime-env: &app_runtime_env NEXT_PUBLIC_STATUS_API_URL_PRODUCTION: ${NEXT_PUBLIC_STATUS_API_URL_PRODUCTION:-} NEXT_PUBLIC_USE_MODELSEED_API: ${NEXT_PUBLIC_USE_MODELSEED_API:-true} NEXT_PUBLIC_USE_NEW_PROXY: ${NEXT_PUBLIC_USE_NEW_PROXY:-true} + NEXT_PUBLIC_PROBMODELSEED_URL: ${NEXT_PUBLIC_PROBMODELSEED_URL:-} services: modelseed-ui-staging: diff --git a/lib/api/config.ts b/lib/api/config.ts index dbe92773..412b9b97 100644 --- a/lib/api/config.ts +++ b/lib/api/config.ts @@ -3,10 +3,52 @@ * Centralized API endpoint configuration. * * RAST / modelseed_support calls remain on legacy endpoints (per backend directive). + * + * IMPORTANT: Next.js only inlines process.env.NEXT_PUBLIC_* when the key is + * statically referenced. We use a static map with explicit property access + * to ensure proper inlining at build time. */ -function readEnv(name: string): string | undefined { +const PUBLIC_ENV = { + NEXT_PUBLIC_DEPLOYMENT_MODE: process.env.NEXT_PUBLIC_DEPLOYMENT_MODE, + NEXT_PUBLIC_SITE_BASE_URL: process.env.NEXT_PUBLIC_SITE_BASE_URL, + NEXT_PUBLIC_SITE_BASE_URL_STAGING: process.env.NEXT_PUBLIC_SITE_BASE_URL_STAGING, + NEXT_PUBLIC_SITE_BASE_URL_PRODUCTION: process.env.NEXT_PUBLIC_SITE_BASE_URL_PRODUCTION, + NEXT_PUBLIC_API_BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL, + NEXT_PUBLIC_API_BASE_URL_STAGING: process.env.NEXT_PUBLIC_API_BASE_URL_STAGING, + NEXT_PUBLIC_API_BASE_URL_PRODUCTION: process.env.NEXT_PUBLIC_API_BASE_URL_PRODUCTION, + NEXT_PUBLIC_REST_BASE_URL: process.env.NEXT_PUBLIC_REST_BASE_URL, + NEXT_PUBLIC_REST_BASE_URL_STAGING: process.env.NEXT_PUBLIC_REST_BASE_URL_STAGING, + NEXT_PUBLIC_REST_BASE_URL_PRODUCTION: process.env.NEXT_PUBLIC_REST_BASE_URL_PRODUCTION, + NEXT_PUBLIC_STATUS_API_URL: process.env.NEXT_PUBLIC_STATUS_API_URL, + NEXT_PUBLIC_STATUS_API_URL_STAGING: process.env.NEXT_PUBLIC_STATUS_API_URL_STAGING, + NEXT_PUBLIC_STATUS_API_URL_PRODUCTION: process.env.NEXT_PUBLIC_STATUS_API_URL_PRODUCTION, + NEXT_PUBLIC_SOLR_BASE_URL: process.env.NEXT_PUBLIC_SOLR_BASE_URL, + NEXT_PUBLIC_SOLR_BASE_URL_STAGING: process.env.NEXT_PUBLIC_SOLR_BASE_URL_STAGING, + NEXT_PUBLIC_SOLR_BASE_URL_PRODUCTION: process.env.NEXT_PUBLIC_SOLR_BASE_URL_PRODUCTION, + NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION: process.env.NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION, + NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_STAGING: process.env.NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_STAGING, + NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_PRODUCTION: process.env.NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_PRODUCTION, + NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION: process.env.NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION, + NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_STAGING: process.env.NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_STAGING, + NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_PRODUCTION: process.env.NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_PRODUCTION, + NEXT_PUBLIC_USE_MODELSEED_API: process.env.NEXT_PUBLIC_USE_MODELSEED_API, + NEXT_PUBLIC_USE_NEW_PROXY: process.env.NEXT_PUBLIC_USE_NEW_PROXY, + NEXT_PUBLIC_PROBMODELSEED_URL: process.env.NEXT_PUBLIC_PROBMODELSEED_URL, +} as const; + +type PublicEnvKey = keyof typeof PUBLIC_ENV; + +function readEnv(name: PublicEnvKey): string | undefined { if (typeof process === 'undefined') return undefined; + return PUBLIC_ENV[name]; +} + +function readEnvSafe(name: string): string | undefined { + if (typeof process === 'undefined') return undefined; + if (name in PUBLIC_ENV) { + return PUBLIC_ENV[name as PublicEnvKey]; + } return process.env[name]; } @@ -58,7 +100,7 @@ function resolveModeValue(params: { productionFallback: string | (() => string); manualDescription: string; }): string { - const overrideValue = toNonEmpty(readEnv(params.overrideVar)); + const overrideValue = toNonEmpty(readEnvSafe(params.overrideVar)); if (overrideValue) return overrideValue; if (DEPLOYMENT_MODE === 'manual') { @@ -68,7 +110,7 @@ function resolveModeValue(params: { const modeDefaultVar = DEPLOYMENT_MODE === 'staging' ? params.stagingDefaultVar : params.productionDefaultVar; - const modeDefaultValue = toNonEmpty(readEnv(modeDefaultVar)); + const modeDefaultValue = toNonEmpty(readEnvSafe(modeDefaultVar)); if (modeDefaultValue) return modeDefaultValue; const fallback = DEPLOYMENT_MODE === 'staging' @@ -155,7 +197,7 @@ export const MODELSEED_API_TEST_URL = stripTrailingSlash( ); function readBooleanEnv(name: string, fallback: boolean): boolean { - const raw = readEnv(name); + const raw = readEnvSafe(name); if (raw === 'true') return true; if (raw === 'false') return false; return fallback; @@ -210,7 +252,7 @@ function resolveSolrCollection(params: { productionFallback: string; description: string; }): string { - const overrideValue = toNonEmpty(readEnv(params.overrideVar)); + const overrideValue = toNonEmpty(readEnvSafe(params.overrideVar)); if (overrideValue) return overrideValue; if (DEPLOYMENT_MODE === 'manual') { @@ -220,7 +262,7 @@ function resolveSolrCollection(params: { const defaultVar = DEPLOYMENT_MODE === 'staging' ? params.stagingDefaultVar : params.productionDefaultVar; - const modeDefault = toNonEmpty(readEnv(defaultVar)); + const modeDefault = toNonEmpty(readEnvSafe(defaultVar)); if (modeDefault) return modeDefault; return DEPLOYMENT_MODE === 'staging' diff --git a/tests/unit/utils/gridFiltering.test.ts b/tests/unit/utils/gridFiltering.test.ts index c132cd58..c84d4c0e 100644 --- a/tests/unit/utils/gridFiltering.test.ts +++ b/tests/unit/utils/gridFiltering.test.ts @@ -1,10 +1,17 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest'; import { GridFilterModel } from '@mui/x-data-grid'; import { filterRowsWithGridModel } from '@/lib/hooks/useToolbarGridFiltering'; describe('grid filtering helpers', () => { + beforeEach(() => { + vi.stubEnv('NEXT_PUBLIC_DEPLOYMENT_MODE', 'staging'); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + it('supports OR logic for column filters', async () => { - process.env.NEXT_PUBLIC_DEPLOYMENT_MODE = 'staging'; const { filterDocsByGridModel } = await import('@/lib/api/biochem'); const rows = [ { id: 'm1', status: 'queued', type: 'model' }, From d99f8733ff3fb2f52ad392e12ecd60ee00b615f6 Mon Sep 17 00:00:00 2001 From: VibhavSetlur Date: Mon, 11 May 2026 15:08:30 -0500 Subject: [PATCH 11/14] docs: add RDKit env var note, verify all env vars documented - Comment out RDKIT in example (has good default in code) - Verify all NEXT_PUBLIC_ vars from config.ts are in .env.example - Total env vars: 33 NEXT_PUBLIC_ vars + 3 PATRIC_ vars --- .env.example | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.env.example b/.env.example index 1ba0d436..1e1a6d75 100644 --- a/.env.example +++ b/.env.example @@ -80,6 +80,13 @@ NEXT_PUBLIC_USE_NEW_PROXY=true # Defaults to {SITE_BASE}/api/model when USE_NEW_PROXY=true NEXT_PUBLIC_PROBMODELSEED_URL= +# ========================= +# RDKit.js URL Override (optional) +# ========================= +# Override for RDKit.js assets (no trailing slash) +# Defaults to unpkg CDN when unset - no need to set unless self-hosting +# NEXT_PUBLIC_RDKIT_BASE_URL= + # ========================= # Build Metadata (/about/version) # ========================= From ff42f02eaf0dd31ae0c1c56cf0b48086e7e2c9e8 Mon Sep 17 00:00:00 2001 From: VibhavSetlur Date: Mon, 11 May 2026 15:12:22 -0500 Subject: [PATCH 12/14] chore: simplify env - remove RDKit var, keep only PATRIC_TOKEN - Remove RDKit env var (has good default in code) - Keep only PATRIC_TOKEN (remove username/password) - Deployment mode: unset=staging, staging, production, manual all verified --- .env.example | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 1e1a6d75..3eff8132 100644 --- a/.env.example +++ b/.env.example @@ -98,6 +98,5 @@ NEXT_PUBLIC_DEPLOY_DATE= # ========================= # Test Credentials # ========================= +# PATRIC token for API testing (get from PATRIC website) PATRIC_TOKEN= -PATRIC_USERNAME= -PATRIC_PASSWORD= From 65db2a41e58cd6066d5954bada8b693508291cd1 Mon Sep 17 00:00:00 2001 From: VibhavSetlur Date: Mon, 11 May 2026 15:16:25 -0500 Subject: [PATCH 13/14] fix: unset deployment mode now requires explicit overrides (manual mode) - Unset NEXT_PUBLIC_DEPLOYMENT_MODE now = manual (strict) mode - Requires override vars or explicit staging/production setting - Updated test to match new behavior --- .env.example | 2 +- lib/api/config.ts | 7 ++++--- tests/unit/api/config.test.ts | 9 ++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index 3eff8132..7bfe261a 100644 --- a/.env.example +++ b/.env.example @@ -9,7 +9,7 @@ # ssh -L 8000:localhost:8000 YOUR_USERNAME@poplar.cels.anl.gov # ========================= -# Deployment Mode +# Deployment Mode (required - set to staging, production, or manual) # ========================= NEXT_PUBLIC_DEPLOYMENT_MODE=staging diff --git a/lib/api/config.ts b/lib/api/config.ts index 412b9b97..c7f44bb2 100644 --- a/lib/api/config.ts +++ b/lib/api/config.ts @@ -78,7 +78,7 @@ function resolveDeploymentMode(raw: string | undefined): DeploymentMode { return normalized; } if (!normalized) { - return 'staging'; + return 'manual'; } throw new Error( `Invalid ${DEPLOYMENT_MODE_VAR} value "${raw}". Use staging, production, or manual.`, @@ -87,8 +87,9 @@ function resolveDeploymentMode(raw: string | undefined): DeploymentMode { function throwManualModeError(overrideVar: string, description: string): never { throw new Error( - `Missing required environment variable ${overrideVar} while ${DEPLOYMENT_MODE_VAR}=manual. ` + - `Set ${overrideVar} (${description}) or set ${DEPLOYMENT_MODE_VAR}=staging|production.`, + `Missing required environment variable ${overrideVar}. ` + + `Set ${DEPLOYMENT_MODE_VAR}=staging|production for default endpoints, ` + + `or set ${overrideVar} (${description}) for manual mode.`, ); } diff --git a/tests/unit/api/config.test.ts b/tests/unit/api/config.test.ts index 7793fa1e..584381db 100644 --- a/tests/unit/api/config.test.ts +++ b/tests/unit/api/config.test.ts @@ -44,12 +44,11 @@ describe('api config deployment resolution', () => { expect(config.SOLR_BASE).toBe('https://custom.modelseed.org/solr/'); }); - it('defaults to staging mode when NEXT_PUBLIC_DEPLOYMENT_MODE is unset', async () => { + it('requires explicit vars when NEXT_PUBLIC_DEPLOYMENT_MODE is unset (manual mode)', async () => { clearEndpointOverrides(); - const config = await loadConfig(); - expect(config.DEPLOYMENT_MODE).toBe('staging'); - expect(config.MODELSEED_SITE_BASE_URL).toBe('https://staging.modelseed.org'); - expect(config.SOLR_REACTIONS_COLLECTION).toBe('reactions_staging'); + vi.stubEnv('NEXT_PUBLIC_SITE_BASE_URL', ''); + vi.stubEnv('NEXT_PUBLIC_API_BASE_URL', ''); + await expect(loadConfig()).rejects.toThrow('NEXT_PUBLIC_SITE_BASE_URL'); }); it('uses production defaults when NEXT_PUBLIC_DEPLOYMENT_MODE=production', async () => { From b14cac59e7ee9f1b04dead6a3b5303e4ad9e709c Mon Sep 17 00:00:00 2001 From: VibhavSetlur Date: Mon, 11 May 2026 15:33:54 -0500 Subject: [PATCH 14/14] fix: revert deployment mode default to staging for build convenience - Unset NEXT_PUBLIC_DEPLOYMENT_MODE now defaults to staging (build-friendly) - Manual mode still requires explicit overrides - Test updated to match behavior --- lib/api/config.ts | 3 ++- tests/unit/api/config.test.ts | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/api/config.ts b/lib/api/config.ts index c7f44bb2..ee1aec4d 100644 --- a/lib/api/config.ts +++ b/lib/api/config.ts @@ -78,7 +78,8 @@ function resolveDeploymentMode(raw: string | undefined): DeploymentMode { return normalized; } if (!normalized) { - return 'manual'; + // Default to staging for build convenience when unset + return 'staging'; } throw new Error( `Invalid ${DEPLOYMENT_MODE_VAR} value "${raw}". Use staging, production, or manual.`, diff --git a/tests/unit/api/config.test.ts b/tests/unit/api/config.test.ts index 584381db..7793fa1e 100644 --- a/tests/unit/api/config.test.ts +++ b/tests/unit/api/config.test.ts @@ -44,11 +44,12 @@ describe('api config deployment resolution', () => { expect(config.SOLR_BASE).toBe('https://custom.modelseed.org/solr/'); }); - it('requires explicit vars when NEXT_PUBLIC_DEPLOYMENT_MODE is unset (manual mode)', async () => { + it('defaults to staging mode when NEXT_PUBLIC_DEPLOYMENT_MODE is unset', async () => { clearEndpointOverrides(); - vi.stubEnv('NEXT_PUBLIC_SITE_BASE_URL', ''); - vi.stubEnv('NEXT_PUBLIC_API_BASE_URL', ''); - await expect(loadConfig()).rejects.toThrow('NEXT_PUBLIC_SITE_BASE_URL'); + const config = await loadConfig(); + expect(config.DEPLOYMENT_MODE).toBe('staging'); + expect(config.MODELSEED_SITE_BASE_URL).toBe('https://staging.modelseed.org'); + expect(config.SOLR_REACTIONS_COLLECTION).toBe('reactions_staging'); }); it('uses production defaults when NEXT_PUBLIC_DEPLOYMENT_MODE=production', async () => {