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 { commentRoutes } from './modules/comments/routes.js';
|
||||||
import { voteRoutes } from './modules/votes/routes.js';
|
import { voteRoutes } from './modules/votes/routes.js';
|
||||||
import { memoryRoutes } from './modules/memory/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 { publicRoutes } from './modules/public/routes.js';
|
||||||
import { tokenRoutes } from './modules/tokens/routes.js';
|
import { tokenRoutes } from './modules/tokens/routes.js';
|
||||||
import { themeRoutes } from './modules/themes/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 { pushTriggerRoutes } from './modules/push-triggers/routes.js';
|
||||||
import { initCosmosIfNeeded } from './lib/cosmos-init.js';
|
import { initCosmosIfNeeded } from './lib/cosmos-init.js';
|
||||||
import { config } from './lib/config.js';
|
import { config } from './lib/config.js';
|
||||||
|
import { seedDefaultFlags } from './modules/flags/seed.js';
|
||||||
|
|
||||||
await initCosmosIfNeeded();
|
await initCosmosIfNeeded();
|
||||||
await loadProductCache();
|
await loadProductCache();
|
||||||
|
|
||||||
|
// Seed default feature flags (idempotent, best-effort)
|
||||||
|
seedDefaultFlags({ info: (msg: string) => console.log(`[flags-seed] ${msg}`) }).catch(() => {});
|
||||||
|
|
||||||
const app = await createServiceApp({
|
const app = await createServiceApp({
|
||||||
name: 'platform-service',
|
name: 'platform-service',
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
@ -132,8 +140,12 @@ await app.register(settingsRoutes, { prefix: '/api' });
|
|||||||
await app.register(itemRoutes, { prefix: '/api' });
|
await app.register(itemRoutes, { prefix: '/api' });
|
||||||
await app.register(commentRoutes, { prefix: '/api' });
|
await app.register(commentRoutes, { prefix: '/api' });
|
||||||
await app.register(voteRoutes, { 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(memoryRoutes, { prefix: '/api' });
|
||||||
|
await app.register(streakRoutes, { prefix: '/api' });
|
||||||
|
await app.register(reflectionRoutes, { prefix: '/api' });
|
||||||
|
await app.register(dailyBriefRoutes, { prefix: '/api' });
|
||||||
// API tokens module
|
// API tokens module
|
||||||
await app.register(tokenRoutes, { prefix: '/api' });
|
await app.register(tokenRoutes, { prefix: '/api' });
|
||||||
// Themes module
|
// Themes module
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user