From e3f23c87138056377253e90e89001f3a40eab99a Mon Sep 17 00:00:00 2001 From: Felix Hennig Date: Fri, 12 Jun 2026 17:44:43 +0200 Subject: [PATCH 1/7] feat(website): introduce DB_ID_SPACE env var for environment-specific collection IDs Wastewater dashboard collection IDs differ between prod, staging, and local environments. Previously the config used isStaging booleans with hardcoded ternaries. This introduces DB_ID_SPACE (prod/staging/local) and a byEnv() helper to make the three-way mapping explicit and readable. Defaults to prod when unset. Co-Authored-By: Claude Sonnet 4.6 --- website/.env.e2e | 1 + website/.env.example | 1 + .../src/components/views/wasap/Wasap.astro | 10 ++-- website/src/env.d.ts | 3 ++ .../header/getPathogenMegaMenuSections.ts | 24 +++++----- website/src/types/dbIdSpace.ts | 21 +++++++++ website/src/types/wastewaterConfig.spec.ts | 4 +- website/src/types/wastewaterConfig.ts | 47 ++++++++++++++----- website/tests/WasapPage.ts | 2 +- website/tests/wastewater.spec.ts | 2 +- 10 files changed, 79 insertions(+), 36 deletions(-) create mode 100644 website/src/types/dbIdSpace.ts diff --git a/website/.env.e2e b/website/.env.e2e index 361558c4f..4c9de97b2 100644 --- a/website/.env.e2e +++ b/website/.env.e2e @@ -1,4 +1,5 @@ CONFIG_DIR=../backend/src/main/resources/ DASHBOARDS_ENVIRONMENT=dashboards-prod +DB_ID_SPACE=local AUTH_SECRET="e2e-test-auth-secret-do-not-use-in-production" diff --git a/website/.env.example b/website/.env.example index 5d27f6ddb..92dd1712b 100644 --- a/website/.env.example +++ b/website/.env.example @@ -1,6 +1,7 @@ CONFIG_DIR=../backend/src/main/resources/ BACKEND_URL=http://localhost:8080 DASHBOARDS_ENVIRONMENT=dashboards-prod +DB_ID_SPACE=prod LOG_DIR=./logs LOG_LEVEL=info diff --git a/website/src/components/views/wasap/Wasap.astro b/website/src/components/views/wasap/Wasap.astro index 39c86ca61..fa4e4e145 100644 --- a/website/src/components/views/wasap/Wasap.astro +++ b/website/src/components/views/wasap/Wasap.astro @@ -2,14 +2,10 @@ import { WasapPage } from './WasapPage'; import { fetchResistanceData, type ResistanceData } from './resistanceData'; import { BackendService } from '../../../backendApi/backendService.ts'; -import { isStaging, getBackendHost } from '../../../config.ts'; +import { getBackendHost } from '../../../config.ts'; import BaseLayout from '../../../layouts/base/BaseLayout.astro'; import { getInstanceLogger } from '../../../logger.ts'; -import { - wastewaterOrganismConfigs, - wastewaterOrganismStagingConfigs, - type WastewaterOrganismName, -} from '../../../types/wastewaterConfig'; +import { type WastewaterOrganismName, wastewaterOrganismConfigs } from '../../../types/wastewaterConfig'; import { getErrorLogMessage } from '../../../util/getErrorLogMessage'; type Props = { @@ -17,7 +13,7 @@ type Props = { }; const { wastewaterOrganism } = Astro.props; -const config = (isStaging() ? wastewaterOrganismStagingConfigs : wastewaterOrganismConfigs)[wastewaterOrganism]; +const config = wastewaterOrganismConfigs()[wastewaterOrganism]; const backendService = new BackendService(getBackendHost()); let resistanceData: ResistanceData = { mutationAnnotations: [], displayMutationsBySet: {} }; diff --git a/website/src/env.d.ts b/website/src/env.d.ts index 4e1118935..d546071bd 100644 --- a/website/src/env.d.ts +++ b/website/src/env.d.ts @@ -17,6 +17,9 @@ declare namespace App { interface ImportMetaEnv { // eslint-disable-next-line @typescript-eslint/naming-convention readonly DASHBOARDS_ENVIRONMENT: 'dashboards-staging' | 'dashboards-prod'; + // TODO - should we use our defined consts below? + // eslint-disable-next-line @typescript-eslint/naming-convention + readonly DB_ID_SPACE: 'prod' | 'staging' | 'local'; } interface ImportMeta { diff --git a/website/src/layouts/base/header/getPathogenMegaMenuSections.ts b/website/src/layouts/base/header/getPathogenMegaMenuSections.ts index 70015a43c..2429deba9 100644 --- a/website/src/layouts/base/header/getPathogenMegaMenuSections.ts +++ b/website/src/layouts/base/header/getPathogenMegaMenuSections.ts @@ -99,24 +99,24 @@ export function getPathogenMegaMenuSections(): PathogenMegaMenuSections { { label: 'SARS-CoV-2', iconType: 'table', - href: wastewaterOrganismConfigs[wastewaterOrganisms.covid].path, - description: wastewaterOrganismConfigs[wastewaterOrganisms.covid].description, + href: wastewaterOrganismConfigs()[wastewaterOrganisms.covid].path, + description: wastewaterOrganismConfigs()[wastewaterOrganisms.covid].description, underlineColor: wastewaterConfig.menuListEntryDecoration, externalLink: false, }, { label: 'RSV-A', iconType: 'table', - href: wastewaterOrganismConfigs[wastewaterOrganisms.rsvA].path, - description: wastewaterOrganismConfigs[wastewaterOrganisms.rsvA].description, + href: wastewaterOrganismConfigs()[wastewaterOrganisms.rsvA].path, + description: wastewaterOrganismConfigs()[wastewaterOrganisms.rsvA].description, underlineColor: wastewaterConfig.menuListEntryDecoration, externalLink: false, }, { label: 'RSV-B', iconType: 'table', - href: wastewaterOrganismConfigs[wastewaterOrganisms.rsvB].path, - description: wastewaterOrganismConfigs[wastewaterOrganisms.rsvB].description, + href: wastewaterOrganismConfigs()[wastewaterOrganisms.rsvB].path, + description: wastewaterOrganismConfigs()[wastewaterOrganisms.rsvB].description, underlineColor: wastewaterConfig.menuListEntryDecoration, externalLink: false, }, @@ -139,24 +139,24 @@ export function getPathogenMegaMenuSections(): PathogenMegaMenuSections { { label: 'Browse SARS-CoV-2 data', iconType: 'database', - href: wastewaterOrganismConfigs[wastewaterOrganisms.covid].browseDataUrl, - description: wastewaterOrganismConfigs[wastewaterOrganisms.covid].browseDataDescription, + href: wastewaterOrganismConfigs()[wastewaterOrganisms.covid].browseDataUrl, + description: wastewaterOrganismConfigs()[wastewaterOrganisms.covid].browseDataDescription, underlineColor: wastewaterConfig.menuListEntryDecoration, externalLink: true, }, { label: 'Browse RSV-A data', iconType: 'database', - href: wastewaterOrganismConfigs[wastewaterOrganisms.rsvA].browseDataUrl, - description: wastewaterOrganismConfigs[wastewaterOrganisms.rsvA].browseDataDescription, + href: wastewaterOrganismConfigs()[wastewaterOrganisms.rsvA].browseDataUrl, + description: wastewaterOrganismConfigs()[wastewaterOrganisms.rsvA].browseDataDescription, underlineColor: wastewaterConfig.menuListEntryDecoration, externalLink: true, }, { label: 'Browse RSV-B data', iconType: 'database', - href: wastewaterOrganismConfigs[wastewaterOrganisms.rsvB].browseDataUrl, - description: wastewaterOrganismConfigs[wastewaterOrganisms.rsvB].browseDataDescription, + href: wastewaterOrganismConfigs()[wastewaterOrganisms.rsvB].browseDataUrl, + description: wastewaterOrganismConfigs()[wastewaterOrganisms.rsvB].browseDataDescription, underlineColor: wastewaterConfig.menuListEntryDecoration, externalLink: true, }, diff --git a/website/src/types/dbIdSpace.ts b/website/src/types/dbIdSpace.ts new file mode 100644 index 000000000..d34b53367 --- /dev/null +++ b/website/src/types/dbIdSpace.ts @@ -0,0 +1,21 @@ +export const dbIdSpaces = { + prod: 'prod', + staging: 'staging', + local: 'local', +} as const; + +export type DbIdSpace = (typeof dbIdSpaces)[keyof typeof dbIdSpaces]; + +export function getDbIdSpace(): DbIdSpace { + const envValue = process.env.DB_ID_SPACE ?? import.meta.env.DB_ID_SPACE; + if (!envValue) { + return dbIdSpaces.prod; + } + const validValues: string[] = [dbIdSpaces.prod, dbIdSpaces.staging, dbIdSpaces.local]; + if (validValues.includes(envValue)) { + return envValue as DbIdSpace; + } + throw new Error( + `Environment variable DB_ID_SPACE (value '${envValue}') is not valid. Expected one of: ${validValues.join(', ')}`, + ); +} diff --git a/website/src/types/wastewaterConfig.spec.ts b/website/src/types/wastewaterConfig.spec.ts index 4671138a5..0991adf03 100644 --- a/website/src/types/wastewaterConfig.spec.ts +++ b/website/src/types/wastewaterConfig.spec.ts @@ -3,7 +3,7 @@ import { describe, expect, test } from 'vitest'; import { wastewaterOrganismConfigs, wastewaterOrganisms } from './wastewaterConfig'; import { enabledAnalysisModes } from '../components/views/wasap/wasapPageConfig'; -describe.each(Object.entries(wastewaterOrganismConfigs))('wastewaterConfig %s', (_configName, config) => { +describe.each(Object.entries(wastewaterOrganismConfigs()))('wastewaterConfig %s', (_configName, config) => { test('default resistance set name is valid', () => { if (config.resistanceAnalysisModeEnabled) { const resistanceSetNames = config.resistanceMutationCollections.map((s) => s.name); @@ -24,7 +24,7 @@ describe.each(Object.entries(wastewaterOrganismConfigs))('wastewaterConfig %s', }); test('COVID wastewater opens on Spike resistance mutations by default', () => { - const covidConfig = wastewaterOrganismConfigs[wastewaterOrganisms.covid]; + const covidConfig = wastewaterOrganismConfigs()[wastewaterOrganisms.covid]; // This pins the default requested for the COVID wastewater dashboard landing state. expect(covidConfig.defaultAnalysisMode).toBe('resistance'); diff --git a/website/src/types/wastewaterConfig.ts b/website/src/types/wastewaterConfig.ts index 85381d4cd..345a4923a 100644 --- a/website/src/types/wastewaterConfig.ts +++ b/website/src/types/wastewaterConfig.ts @@ -1,8 +1,21 @@ import type { MutationAnnotation } from '@genspectrum/dashboard-components/util'; +import { dbIdSpaces, type DbIdSpace } from './dbIdSpace'; +import { getDbIdSpace } from './dbIdSpace'; import type { ResistanceMutationCollectionConfig } from '../components/views/wasap/wasapPageConfig'; import { VARIANT_TIME_FRAME, type WasapPageConfig } from '../components/views/wasap/wasapPageConfig'; +function byEnv(env: DbIdSpace, prod: T, staging: T, local: T): T { + switch (env) { + case dbIdSpaces.prod: + return prod; + case dbIdSpaces.staging: + return staging; + case dbIdSpaces.local: + return local; + } +} + export const wastewaterOrganisms = { covid: 'covid', rsvA: 'rsv-a', @@ -13,7 +26,7 @@ export type WastewaterOrganismName = (typeof wastewaterOrganisms)[keyof typeof w export const wastewaterPathFragment = 'swiss-wastewater'; -function buildWastewaterOrganismConfigs(isStaging: boolean): Record { +function buildWastewaterOrganismConfigs(env: DbIdSpace): Record { return { [wastewaterOrganisms.covid]: { internalName: wastewaterOrganisms.covid, @@ -34,21 +47,21 @@ function buildWastewaterOrganismConfigs(isStaging: boolean): RecordStanford Coronavirus Antiviral & Resistance database (last updated on 21 August 2024).', }, { - collectionId: isStaging ? 2 : 5, + collectionId: byEnv(env, 5, 2, 2), name: 'RdRp', annotationSymbol: 'r', description: 'SARS-CoV-2 RNA-dependent RNA polymerase (RdRP) inhibitor resistance mutation as per Stanford Coronavirus Antiviral & Resistance database (last updated on 21 August 2024).', }, { - collectionId: isStaging ? 3 : 6, + collectionId: byEnv(env, 6, 3, 3), name: 'Spike', annotationSymbol: 's', description: @@ -59,7 +72,7 @@ function buildWastewaterOrganismConfigs(isStaging: boolean): RecordViralZone.', }, { - collectionId: isStaging ? 5 : 4984, + collectionId: byEnv(env, 4984, 5, 5), name: 'Palivizumab', annotationSymbol: 'p', description: @@ -201,14 +214,14 @@ function buildWastewaterOrganismConfigs(isStaging: boolean): RecordViralZone.', }, { - collectionId: isStaging ? 7 : 4986, + collectionId: byEnv(env, 4986, 7, 7), name: 'Palivizumab', annotationSymbol: 'p', description: @@ -241,8 +254,16 @@ function buildWastewaterOrganismConfigs(isStaging: boolean): Record { test.setTimeout(60_000); for (const organism of Object.values(wastewaterOrganisms)) { - const { name } = wastewaterOrganismConfigs[organism]; + const { name } = wastewaterOrganismConfigs()[organism]; test.describe(name, () => { test('should load with default filters and show mutation data', async ({ wasapPage }) => { From 1fe3dbeadc6b1b0575c8e30584ff89bcb6caeeba Mon Sep 17 00:00:00 2001 From: Felix Hennig Date: Wed, 24 Jun 2026 14:51:22 +0200 Subject: [PATCH 2/7] refactor(website): change byEnv to use named-object params instead of positional Co-Authored-By: Claude Sonnet 4.6 --- website/src/types/wastewaterConfig.ts | 28 +++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/website/src/types/wastewaterConfig.ts b/website/src/types/wastewaterConfig.ts index 345a4923a..6c59a5946 100644 --- a/website/src/types/wastewaterConfig.ts +++ b/website/src/types/wastewaterConfig.ts @@ -5,14 +5,14 @@ import { getDbIdSpace } from './dbIdSpace'; import type { ResistanceMutationCollectionConfig } from '../components/views/wasap/wasapPageConfig'; import { VARIANT_TIME_FRAME, type WasapPageConfig } from '../components/views/wasap/wasapPageConfig'; -function byEnv(env: DbIdSpace, prod: T, staging: T, local: T): T { +function byEnv(env: DbIdSpace, vars: { prod: T; staging: T; local: T }): T { switch (env) { case dbIdSpaces.prod: - return prod; + return vars.prod; case dbIdSpaces.staging: - return staging; + return vars.staging; case dbIdSpaces.local: - return local; + return vars.local; } } @@ -47,21 +47,21 @@ function buildWastewaterOrganismConfigs(env: DbIdSpace): RecordStanford Coronavirus Antiviral & Resistance database (last updated on 21 August 2024).', }, { - collectionId: byEnv(env, 5, 2, 2), + collectionId: byEnv(env, { prod: 5, staging: 2, local: 2 }), name: 'RdRp', annotationSymbol: 'r', description: 'SARS-CoV-2 RNA-dependent RNA polymerase (RdRP) inhibitor resistance mutation as per Stanford Coronavirus Antiviral & Resistance database (last updated on 21 August 2024).', }, { - collectionId: byEnv(env, 6, 3, 3), + collectionId: byEnv(env, { prod: 6, staging: 3, local: 3 }), name: 'Spike', annotationSymbol: 's', description: @@ -72,7 +72,7 @@ function buildWastewaterOrganismConfigs(env: DbIdSpace): RecordViralZone.', }, { - collectionId: byEnv(env, 4984, 5, 5), + collectionId: byEnv(env, { prod: 4984, staging: 5, local: 5 }), name: 'Palivizumab', annotationSymbol: 'p', description: @@ -214,14 +214,14 @@ function buildWastewaterOrganismConfigs(env: DbIdSpace): RecordViralZone.', }, { - collectionId: byEnv(env, 4986, 7, 7), + collectionId: byEnv(env, { prod: 4986, staging: 7, local: 7 }), name: 'Palivizumab', annotationSymbol: 'p', description: From d3404aa65ec440d0b1f7e998490dc6270f76f2b3 Mon Sep 17 00:00:00 2001 From: Felix Hennig Date: Wed, 24 Jun 2026 14:57:40 +0200 Subject: [PATCH 3/7] fix(website): use correct local collection ID for COVID variant filter default (XFG, not XEC.35.1) Co-Authored-By: Claude Sonnet 4.6 --- website/src/types/wastewaterConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/types/wastewaterConfig.ts b/website/src/types/wastewaterConfig.ts index 6c59a5946..5213d2110 100644 --- a/website/src/types/wastewaterConfig.ts +++ b/website/src/types/wastewaterConfig.ts @@ -103,7 +103,7 @@ function buildWastewaterOrganismConfigs(env: DbIdSpace): Record Date: Wed, 24 Jun 2026 14:58:30 +0200 Subject: [PATCH 4/7] fix(e2e): set DB_ID_SPACE=local in website Docker container for E2E tests Co-Authored-By: Claude Sonnet 4.6 --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 16e0848a5..3844a499b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,7 @@ services: environment: BACKEND_URL: http://backend:8080 DASHBOARDS_ENVIRONMENT: dashboards-prod + DB_ID_SPACE: local GITHUB_CLIENT_ID: "dummy" GITHUB_CLIENT_SECRET: "dummy" AUTH_SECRET: "e2e-test-auth-secret-do-not-use-in-production" From 92fa19f162fc63e937eb58fb310e0f94423f5c99 Mon Sep 17 00:00:00 2001 From: Felix Hennig Date: Wed, 24 Jun 2026 16:07:49 +0200 Subject: [PATCH 5/7] refactor(website): use DbIdSpace type in env.d.ts instead of inlining the literal union Co-Authored-By: Claude Sonnet 4.6 --- website/src/env.d.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/website/src/env.d.ts b/website/src/env.d.ts index d546071bd..eacc95d83 100644 --- a/website/src/env.d.ts +++ b/website/src/env.d.ts @@ -17,9 +17,8 @@ declare namespace App { interface ImportMetaEnv { // eslint-disable-next-line @typescript-eslint/naming-convention readonly DASHBOARDS_ENVIRONMENT: 'dashboards-staging' | 'dashboards-prod'; - // TODO - should we use our defined consts below? // eslint-disable-next-line @typescript-eslint/naming-convention - readonly DB_ID_SPACE: 'prod' | 'staging' | 'local'; + readonly DB_ID_SPACE: import('./types/dbIdSpace').DbIdSpace; } interface ImportMeta { From 993f7877e23a459acaefcca97f6b6290b6502bcc Mon Sep 17 00:00:00 2001 From: Felix Hennig Date: Wed, 24 Jun 2026 16:14:48 +0200 Subject: [PATCH 6/7] refactor(website): derive DB_ID_SPACE from DASHBOARDS_ENVIRONMENT by default DB_ID_SPACE is now an optional override. When unset, it falls back to 'staging' if DASHBOARDS_ENVIRONMENT is 'dashboards-staging', and 'prod' otherwise. 'local' still requires an explicit DB_ID_SPACE=local. Co-Authored-By: Claude Sonnet 4.6 --- website/.env.example | 2 +- website/src/env.d.ts | 2 +- website/src/types/dbIdSpace.ts | 19 ++++++++++--------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/website/.env.example b/website/.env.example index 92dd1712b..ce30fcdc7 100644 --- a/website/.env.example +++ b/website/.env.example @@ -1,7 +1,7 @@ CONFIG_DIR=../backend/src/main/resources/ BACKEND_URL=http://localhost:8080 DASHBOARDS_ENVIRONMENT=dashboards-prod -DB_ID_SPACE=prod +# DB_ID_SPACE=local # optional override; defaults to 'prod'/'staging' based on DASHBOARDS_ENVIRONMENT LOG_DIR=./logs LOG_LEVEL=info diff --git a/website/src/env.d.ts b/website/src/env.d.ts index eacc95d83..b4283caeb 100644 --- a/website/src/env.d.ts +++ b/website/src/env.d.ts @@ -18,7 +18,7 @@ interface ImportMetaEnv { // eslint-disable-next-line @typescript-eslint/naming-convention readonly DASHBOARDS_ENVIRONMENT: 'dashboards-staging' | 'dashboards-prod'; // eslint-disable-next-line @typescript-eslint/naming-convention - readonly DB_ID_SPACE: import('./types/dbIdSpace').DbIdSpace; + readonly DB_ID_SPACE?: import('./types/dbIdSpace').DbIdSpace; } interface ImportMeta { diff --git a/website/src/types/dbIdSpace.ts b/website/src/types/dbIdSpace.ts index d34b53367..be1828a16 100644 --- a/website/src/types/dbIdSpace.ts +++ b/website/src/types/dbIdSpace.ts @@ -8,14 +8,15 @@ export type DbIdSpace = (typeof dbIdSpaces)[keyof typeof dbIdSpaces]; export function getDbIdSpace(): DbIdSpace { const envValue = process.env.DB_ID_SPACE ?? import.meta.env.DB_ID_SPACE; - if (!envValue) { - return dbIdSpaces.prod; + if (envValue) { + const validValues: string[] = [dbIdSpaces.prod, dbIdSpaces.staging, dbIdSpaces.local]; + if (validValues.includes(envValue)) { + return envValue as DbIdSpace; + } + throw new Error( + `Environment variable DB_ID_SPACE (value '${envValue}') is not valid. Expected one of: ${validValues.join(', ')}`, + ); } - const validValues: string[] = [dbIdSpaces.prod, dbIdSpaces.staging, dbIdSpaces.local]; - if (validValues.includes(envValue)) { - return envValue as DbIdSpace; - } - throw new Error( - `Environment variable DB_ID_SPACE (value '${envValue}') is not valid. Expected one of: ${validValues.join(', ')}`, - ); + const dashboardsEnv = process.env.DASHBOARDS_ENVIRONMENT ?? import.meta.env.DASHBOARDS_ENVIRONMENT; + return dashboardsEnv === 'dashboards-staging' ? dbIdSpaces.staging : dbIdSpaces.prod; } From 67de716122a029f93f3748e709661e23ca377607 Mon Sep 17 00:00:00 2001 From: Felix Hennig Date: Thu, 25 Jun 2026 14:53:09 +0200 Subject: [PATCH 7/7] review --- website/src/types/wastewaterConfig.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/website/src/types/wastewaterConfig.ts b/website/src/types/wastewaterConfig.ts index 5213d2110..cfb5f3a35 100644 --- a/website/src/types/wastewaterConfig.ts +++ b/website/src/types/wastewaterConfig.ts @@ -1,7 +1,6 @@ import type { MutationAnnotation } from '@genspectrum/dashboard-components/util'; -import { dbIdSpaces, type DbIdSpace } from './dbIdSpace'; -import { getDbIdSpace } from './dbIdSpace'; +import { getDbIdSpace, dbIdSpaces, type DbIdSpace } from './dbIdSpace'; import type { ResistanceMutationCollectionConfig } from '../components/views/wasap/wasapPageConfig'; import { VARIANT_TIME_FRAME, type WasapPageConfig } from '../components/views/wasap/wasapPageConfig'; @@ -117,7 +116,7 @@ function buildWastewaterOrganismConfigs(env: DbIdSpace): Record