From 5bba149a7b7acc9db6114dc39b97ac5fdd1e1fe4 Mon Sep 17 00:00:00 2001 From: Saravana Achu Mac Date: Sat, 4 Apr 2026 18:10:34 -0700 Subject: [PATCH] refactor: share feature flag contract across backend and web --- backend/src/services/apiServer.ts | 6 ++++-- backend/tsconfig.json | 11 ++++++----- docs/OPERATIONS.md | 3 ++- docs/ROADMAP.md | 1 + shared/feature-flags.ts | 15 +++++++++++++++ web/src/backtest/flags.ts | 7 ++++--- web/src/tabs/ConfigTab.tsx | 6 +----- 7 files changed, 33 insertions(+), 16 deletions(-) create mode 100644 shared/feature-flags.ts diff --git a/backend/src/services/apiServer.ts b/backend/src/services/apiServer.ts index ecc5d64..1d266cb 100644 --- a/backend/src/services/apiServer.ts +++ b/backend/src/services/apiServer.ts @@ -48,6 +48,7 @@ import { canonicalLifecycleService, type CanonicalLifecycleProfileMeta } from './canonicalLifecycleService.js'; +import type { TradingFeatureFlagsResponse } from '../../../shared/feature-flags.js'; interface AuthenticatedRequest extends Request { authUserId?: string; @@ -1645,14 +1646,15 @@ export class ApiServer { }); this.app.get('/api/feature-flags', this.requireAuth, (_req, res) => { - res.json({ + const flags: TradingFeatureFlagsResponse = { backtest: { enableBacktest: Boolean(config.ENABLE_BACKTEST), customerEnabled: Boolean(config.BACKTEST_CUSTOMER_ENABLED), maxCsvBytes: Number(config.BACKTEST_MAX_CSV_BYTES), maxRows: Number(config.BACKTEST_MAX_ROWS), } - }); + }; + res.json(flags); }); this.app.get('/api/me/profile', this.requireAuth, async (req, res) => { diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 88e8dec..d40f413 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -4,16 +4,17 @@ "module": "NodeNext", "moduleResolution": "NodeNext", "outDir": "./dist", - "rootDir": "./src", + "rootDir": "..", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, - "include": [ - "src/**/*" - ], + "include": [ + "src/**/*", + "../shared/**/*.ts" + ], "exclude": [ "src/test_*" ] -} \ No newline at end of file +} diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 351d0f1..ee74717 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -228,6 +228,7 @@ Manual mobile release smoke is still required before broad rollout: - web now uses platform-session handling end to end; the remaining auth cleanup is removing dormant compatibility stubs and aligning profile bootstrap contracts fully with backend-owned product APIs - root `pnpm verify` is green again after aligning the web Vitest harness with platform-session storage and current API contracts - mobile does not yet include push notification infrastructure -- feature-flag ownership and exchange/order-level correlation-ID propagation are not fully standardized yet +- broader feature-flag ownership beyond the current shared backtest contract is not fully standardized yet +- exchange/order-level correlation-ID propagation is not fully standardized yet These are follow-up items, not hidden defects. They should remain tracked in `docs/ROADMAP.md`. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 9bef15c..e6f19e5 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -42,6 +42,7 @@ It assumes: - [x] Release smoke coverage now exists for web auth and product accessibility flows, with a tracked mobile release smoke checklist in operations - [x] Request ID propagation is now standardized across the main web/mobile API paths, operator actions, lifecycle fetches, and backend HTTP responses - [x] Backtest feature access now reads from an explicit backend feature-flags contract instead of scraping generic runtime config +- [x] Feature-flag key ownership and response shape for backtest are now shared across backend and web through a common product contract - [x] Trading user profiles and marketplace presets now have Cosmos-backed authority paths - [x] Runtime order, trade-history, manual-entry, order-activity, and reconciliation-audit repositories now use Cosmos-backed trading-record storage instead of the legacy service layer - [x] Runtime sub-tag repair now operates through the Cosmos-backed order repository diff --git a/shared/feature-flags.ts b/shared/feature-flags.ts new file mode 100644 index 0000000..3c53085 --- /dev/null +++ b/shared/feature-flags.ts @@ -0,0 +1,15 @@ +export const BACKTEST_FLAG_KEYS = { + ENABLE_BACKTEST: 'ENABLE_BACKTEST', + BACKTEST_CUSTOMER_ENABLED: 'BACKTEST_CUSTOMER_ENABLED', +} as const; + +export interface BacktestFeatureFlags { + enableBacktest: boolean; + customerEnabled: boolean; + maxCsvBytes?: number; + maxRows?: number; +} + +export interface TradingFeatureFlagsResponse { + backtest: BacktestFeatureFlags; +} diff --git a/web/src/backtest/flags.ts b/web/src/backtest/flags.ts index 5c6b915..839fefd 100644 --- a/web/src/backtest/flags.ts +++ b/web/src/backtest/flags.ts @@ -1,10 +1,11 @@ import { getPlatformAccessToken } from '../lib/authSession'; import { tradingRuntime } from '../lib/runtime'; import { createRequestId } from '../../../shared/request-id.js'; +import type { TradingFeatureFlagsResponse } from '../../../shared/feature-flags.js'; export interface BacktestRuntimeFlags { - enableBacktest: boolean; - customerEnabled: boolean; + enableBacktest: boolean; + customerEnabled: boolean; } const CACHE_TTL_MS = 15_000; @@ -64,7 +65,7 @@ export const loadBacktestRuntimeFlags = async (): Promise throw new Error(`Failed to fetch feature flags (${response.status})`); } - const body = await response.json().catch(() => ({} as any)); + const body = await response.json().catch(() => ({} as TradingFeatureFlagsResponse)); const loaded: BacktestRuntimeFlags = { enableBacktest: toBoolean(body?.backtest?.enableBacktest, false), customerEnabled: toBoolean(body?.backtest?.customerEnabled, false) diff --git a/web/src/tabs/ConfigTab.tsx b/web/src/tabs/ConfigTab.tsx index bdcfd51..6abd85c 100644 --- a/web/src/tabs/ConfigTab.tsx +++ b/web/src/tabs/ConfigTab.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { AlertCircle, CheckCircle2, LayoutDashboard, Info, Zap, Key, ShieldAlert } from 'lucide-react'; import { clearBacktestRuntimeFlagCache } from '../backtest/flags'; import { fetchDynamicConfigItems, upsertDynamicConfigItems } from '../lib/dynamicConfigApi'; +import { BACKTEST_FLAG_KEYS } from '../../../shared/feature-flags.js'; interface ConfigItem { key: string; @@ -14,11 +15,6 @@ interface BacktestFlagState { customerEnabled: boolean; } -const BACKTEST_FLAG_KEYS = { - ENABLE_BACKTEST: 'ENABLE_BACKTEST', - BACKTEST_CUSTOMER_ENABLED: 'BACKTEST_CUSTOMER_ENABLED' -} as const; - const readBooleanConfig = (configs: ConfigItem[], key: string, fallback: boolean = false): boolean => { const row = configs.find((item) => item.key === key); if (!row) return fallback;