feat(flags): seed default feature flags for all products on startup
This commit is contained in:
parent
711621e3d9
commit
a97b730a89
189
services/platform-service/src/modules/flags/seed.ts
Normal file
189
services/platform-service/src/modules/flags/seed.ts
Normal file
@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Feature flag seeding — creates default flags per product on startup.
|
||||
*
|
||||
* Idempotent: skips flags that already exist (matched by key + productId).
|
||||
* Called from server startup after Cosmos init.
|
||||
*/
|
||||
|
||||
import * as repo from './repository.js';
|
||||
import type { FeatureFlagDoc } from './types.js';
|
||||
|
||||
interface FlagSeedDef {
|
||||
key: string;
|
||||
enabled: boolean;
|
||||
description: string;
|
||||
platforms: string[];
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
// ─── Default flags per product ───────────────────────────────────────────────
|
||||
|
||||
const COMMON_FLAGS: FlagSeedDef[] = [
|
||||
{
|
||||
key: 'maintenance_mode',
|
||||
enabled: false,
|
||||
description: 'Global maintenance mode — disables non-essential features',
|
||||
platforms: [],
|
||||
percentage: 100,
|
||||
},
|
||||
{
|
||||
key: 'telemetry_enabled',
|
||||
enabled: true,
|
||||
description: 'Client telemetry collection enabled',
|
||||
platforms: [],
|
||||
percentage: 100,
|
||||
},
|
||||
];
|
||||
|
||||
const PRODUCT_FLAGS: Record<string, FlagSeedDef[]> = {
|
||||
chronomind: [
|
||||
{
|
||||
key: 'routines_enabled',
|
||||
enabled: true,
|
||||
description: 'Routines feature visible in navigation',
|
||||
platforms: ['web', 'ios', 'android'],
|
||||
percentage: 100,
|
||||
},
|
||||
{
|
||||
key: 'focus_mode_enabled',
|
||||
enabled: true,
|
||||
description: 'Pomodoro / focus mode available',
|
||||
platforms: ['web', 'ios', 'android'],
|
||||
percentage: 100,
|
||||
},
|
||||
{
|
||||
key: 'calendar_import_enabled',
|
||||
enabled: true,
|
||||
description: 'Calendar .ics import feature',
|
||||
platforms: ['web'],
|
||||
percentage: 100,
|
||||
},
|
||||
{
|
||||
key: 'cloud_sync_enabled',
|
||||
enabled: false,
|
||||
description: 'Cloud sync for timers and routines',
|
||||
platforms: [],
|
||||
percentage: 0,
|
||||
},
|
||||
],
|
||||
nomgap: [
|
||||
{
|
||||
key: 'ai_coach_enabled',
|
||||
enabled: false,
|
||||
description: 'AI coaching card on fasting screen (Pro feature)',
|
||||
platforms: ['ios', 'android'],
|
||||
percentage: 0,
|
||||
},
|
||||
{
|
||||
key: 'social_fasting_enabled',
|
||||
enabled: false,
|
||||
description: 'Social fasting / group fasts',
|
||||
platforms: ['ios', 'android'],
|
||||
percentage: 0,
|
||||
},
|
||||
{
|
||||
key: 'meal_logging_enabled',
|
||||
enabled: true,
|
||||
description: 'Meal logging before/after fasts',
|
||||
platforms: ['ios', 'android'],
|
||||
percentage: 100,
|
||||
},
|
||||
{
|
||||
key: 'anatomical_mode_enabled',
|
||||
enabled: false,
|
||||
description: 'Anatomical body visualization (Pro)',
|
||||
platforms: ['ios', 'android'],
|
||||
percentage: 0,
|
||||
},
|
||||
],
|
||||
mindlyst: [
|
||||
{
|
||||
key: 'voice_capture_enabled',
|
||||
enabled: true,
|
||||
description: 'Voice capture / transcription',
|
||||
platforms: ['ios', 'android'],
|
||||
percentage: 100,
|
||||
},
|
||||
{
|
||||
key: 'ai_triage_enabled',
|
||||
enabled: true,
|
||||
description: 'AI auto-triage into brains',
|
||||
platforms: ['ios', 'android', 'web'],
|
||||
percentage: 100,
|
||||
},
|
||||
{
|
||||
key: 'share_cards_enabled',
|
||||
enabled: false,
|
||||
description: 'Shareable memory cards',
|
||||
platforms: [],
|
||||
percentage: 0,
|
||||
},
|
||||
],
|
||||
lysnrai: [
|
||||
{
|
||||
key: 'keyboard_dictation_enabled',
|
||||
enabled: true,
|
||||
description: 'Keyboard extension dictation',
|
||||
platforms: ['ios'],
|
||||
percentage: 100,
|
||||
},
|
||||
{
|
||||
key: 'smart_punctuation_enabled',
|
||||
enabled: true,
|
||||
description: 'Smart punctuation in dictation',
|
||||
platforms: ['ios', 'desktop'],
|
||||
percentage: 100,
|
||||
},
|
||||
{
|
||||
key: 'speak_to_translate_enabled',
|
||||
enabled: false,
|
||||
description: 'Speak-to-translate mode (requires Azure Translator)',
|
||||
platforms: ['ios'],
|
||||
percentage: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// ─── Seed function ───────────────────────────────────────────────────────────
|
||||
|
||||
export async function seedDefaultFlags(logger?: { info: (msg: string) => void }): Promise<number> {
|
||||
const log = logger ?? { info: () => {} };
|
||||
let created = 0;
|
||||
|
||||
const products = Object.keys(PRODUCT_FLAGS);
|
||||
|
||||
for (const productId of products) {
|
||||
const flags = [...COMMON_FLAGS, ...PRODUCT_FLAGS[productId]];
|
||||
|
||||
for (const def of flags) {
|
||||
const existing = await repo.getByKey(def.key, productId);
|
||||
if (existing) continue;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const doc: FeatureFlagDoc = {
|
||||
id: `flag_${productId}_${def.key}`,
|
||||
productId,
|
||||
key: def.key,
|
||||
enabled: def.enabled,
|
||||
description: def.description,
|
||||
platforms: def.platforms,
|
||||
regions: [],
|
||||
osVersions: [],
|
||||
segments: [],
|
||||
percentage: def.percentage,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
await repo.create(doc);
|
||||
created++;
|
||||
log.info(`Seeded flag: ${productId}/${def.key} (enabled=${def.enabled})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (created > 0) {
|
||||
log.info(`Feature flag seeding complete: ${created} flags created`);
|
||||
}
|
||||
|
||||
return created;
|
||||
}
|
||||
@ -42,6 +42,10 @@ import { itemRoutes } from './modules/items/routes.js';
|
||||
import { commentRoutes } from './modules/comments/routes.js';
|
||||
import { voteRoutes } from './modules/votes/routes.js';
|
||||
import { memoryRoutes } from './modules/memory/routes.js';
|
||||
import { brainRoutes } from './modules/brains/routes.js';
|
||||
import { streakRoutes } from './modules/streaks/routes.js';
|
||||
import { reflectionRoutes } from './modules/reflections/routes.js';
|
||||
import { dailyBriefRoutes } from './modules/daily-briefs/routes.js';
|
||||
import { publicRoutes } from './modules/public/routes.js';
|
||||
import { tokenRoutes } from './modules/tokens/routes.js';
|
||||
import { themeRoutes } from './modules/themes/routes.js';
|
||||
@ -72,10 +76,14 @@ import { changelogRoutes } from './modules/changelog/routes.js';
|
||||
import { pushTriggerRoutes } from './modules/push-triggers/routes.js';
|
||||
import { initCosmosIfNeeded } from './lib/cosmos-init.js';
|
||||
import { config } from './lib/config.js';
|
||||
import { seedDefaultFlags } from './modules/flags/seed.js';
|
||||
|
||||
await initCosmosIfNeeded();
|
||||
await loadProductCache();
|
||||
|
||||
// Seed default feature flags (idempotent, best-effort)
|
||||
seedDefaultFlags({ info: (msg: string) => console.log(`[flags-seed] ${msg}`) }).catch(() => {});
|
||||
|
||||
const app = await createServiceApp({
|
||||
name: 'platform-service',
|
||||
version: '0.1.0',
|
||||
@ -132,8 +140,12 @@ await app.register(settingsRoutes, { prefix: '/api' });
|
||||
await app.register(itemRoutes, { prefix: '/api' });
|
||||
await app.register(commentRoutes, { prefix: '/api' });
|
||||
await app.register(voteRoutes, { prefix: '/api' });
|
||||
// Mobile capture modules
|
||||
// MindLyst modules (brains, memory, streaks, reflections, daily briefs)
|
||||
await app.register(brainRoutes, { prefix: '/api' });
|
||||
await app.register(memoryRoutes, { prefix: '/api' });
|
||||
await app.register(streakRoutes, { prefix: '/api' });
|
||||
await app.register(reflectionRoutes, { prefix: '/api' });
|
||||
await app.register(dailyBriefRoutes, { prefix: '/api' });
|
||||
// API tokens module
|
||||
await app.register(tokenRoutes, { prefix: '/api' });
|
||||
// Themes module
|
||||
|
||||
Loading…
Reference in New Issue
Block a user