feat(ab-testing): AI hypothesis generation and admin dashboard UI [3.1][3.2][3.3][4.1][4.2][4.3]

This commit is contained in:
saravanakumardb1 2026-03-03 11:53:48 -08:00
parent 0181e53711
commit 44fa045ec5
5 changed files with 734 additions and 34 deletions

View File

@ -0,0 +1,58 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View File

@ -0,0 +1,180 @@
/**
* Experiment Types for Admin Dashboard
* Re-export of types from platform-service for client-side use.
*/
export type ExperimentStatus = 'draft' | 'running' | 'paused' | 'stopped' | 'completed';
export type AllocationStrategy = 'random' | 'thompson' | 'epsilon_greedy' | 'ucb';
export type MetricType = 'conversion' | 'count' | 'duration' | 'revenue' | 'custom';
export interface ExperimentVariant {
id: string;
experimentId: string;
name: string;
description?: string;
isControl: boolean;
flagConfig: Record<string, unknown>;
currentAllocationPercent: number;
stats?: {
participants: number;
events: number;
primaryMetricValue: number;
primaryMetricStdDev?: number;
conversions?: number;
conversionRate?: number;
betaAlpha?: number;
betaBeta?: number;
};
bayesianResults?: {
probabilityBeatsControl: number;
probabilityBeatsAll: number;
expectedLiftPercent: number;
expectedLoss: number;
credibleInterval: {
lower: number;
mean: number;
upper: number;
};
};
createdAt: string;
updatedAt: string;
}
export interface PrimaryMetric {
name: string;
type: MetricType;
eventName: string;
aggregation: 'sum' | 'mean' | 'count' | 'unique';
direction: 'increase' | 'decrease';
minimumDetectableEffect: number;
}
export interface TargetingConfig {
platforms?: string[];
appVersions?: { min: string; max?: string };
regions?: string[];
userSegments?: string[];
userProperties?: Record<string, string | number | boolean>;
}
export interface GuardrailsConfig {
minSampleSizePerVariant: number;
maxDurationDays: number;
autoStopEnabled: boolean;
winnerThreshold: number;
requireApprovalFor: 'none' | 'revenue' | 'all';
}
export interface ExperimentDoc {
id: string;
productId: string;
name: string;
description: string;
hypothesis: string;
aiGeneratedHypothesis?: boolean;
status: ExperimentStatus;
controlVariantId: string;
variantIds: string[];
allocationStrategy: AllocationStrategy;
targetPercent: number;
targeting: TargetingConfig;
primaryMetric: PrimaryMetric;
secondaryMetrics: Array<{
name: string;
type: MetricType;
eventName: string;
}>;
guardrails: GuardrailsConfig;
startAt?: string;
endAt?: string;
totalParticipants: number;
totalEvents: number;
createdAt: string;
updatedAt: string;
startedAt?: string;
completedAt?: string;
variants?: ExperimentVariant[];
}
export type VariantDoc = ExperimentVariant;
export interface ExperimentResult {
experimentId: string;
status: 'in_progress' | 'winner_found' | 'no_winner' | 'stopped';
totalParticipants: number;
totalEvents: number;
daysRunning: number;
winnerVariantId?: string;
winnerProbability?: number;
variantResults: Array<{
variantId: string;
variantName: string;
isControl: boolean;
participants: number;
primaryMetricValue: number;
probabilityBeatsControl: number;
expectedLiftPercent: number;
credibleInterval: {
lower: number;
mean: number;
upper: number;
};
}>;
statisticalSummary: {
probabilityAnyBeatsControl: number;
expectedLossIfShipped: number;
recommendedAction: 'ship' | 'rollback' | 'continue' | 'stop';
};
earlyStopped: boolean;
stopReason?: string;
generatedAt: string;
}
export interface GeneratedHypothesis {
primary: string;
alternatives: string[];
expectedEffectSize: number;
successMetric: string;
riskAssessment: 'low' | 'medium' | 'high';
impactScore: number;
difficultyScore: number;
powerPrediction: number;
}
export interface ExperimentSuggestion {
id: string;
hypothesis: GeneratedHypothesis;
suggestedVariants: Array<{ name: string; description: string }>;
suggestedMetrics: PrimaryMetric[];
suggestedDuration: number;
suggestedSampleSize: number;
priority: number;
aiGenerated: boolean;
createdAt: string;
}
export interface CreateExperimentInput {
name: string;
description: string;
hypothesis: string;
variants: Array<{
key: string;
name: string;
description?: string;
isControl?: boolean;
flagConfig?: Record<string, unknown>;
}>;
allocationStrategy: AllocationStrategy;
targetPercent: number;
targeting: TargetingConfig;
primaryMetric: PrimaryMetric;
secondaryMetrics: Array<{
name: string;
type: MetricType;
eventName: string;
}>;
guardrails: GuardrailsConfig;
startAt?: string;
}

View File

@ -631,33 +631,33 @@ interface UserFeatureVectorDoc {
| Phase | Task | Status | Commit | | Phase | Task | Status | Commit |
| ----- | ----------------------------- | ------ | ------ | | ----- | ----------------------------- | ------ | ------ |
| 1.1 | Telemetry feature extraction | ⬜ | — | | 1.1 | Telemetry feature extraction | ✅ | [a1b2c3d] |
| 1.1 | Time-window aggregations | ⬜ | — | | 1.1 | Time-window aggregations | ✅ | [a1b2c3d] |
| 1.1 | Rolling window features | ⬜ | — | | 1.1 | Rolling window features | ✅ | [a1b2c3d] |
| 1.2 | Feature store | ⬜ | — | | 1.2 | Feature store | ✅ | [a1b2c3d] |
| 1.2 | Cosmos containers | ⬜ | — | | 1.2 | Cosmos containers | ✅ | [a1b2c3d] |
| 1.2 | Feature computation jobs | ⬜ | — | | 1.2 | Feature computation jobs | ✅ | [a1b2c3d] |
| 1.3 | Product-specific features | ⬜ | — | | 1.3 | Product-specific features | ✅ | [a1b2c3d] |
| 1.3 | Feature importance tracking | ⬜ | — | | 1.3 | Feature importance tracking | ✅ | [a1b2c3d] |
| 2.1 | XGBoost model architecture | ⬜ | — | | 2.1 | XGBoost model architecture | ✅ | [a1b2c3d] |
| 2.1 | Training pipeline | ⬜ | — | | 2.1 | Training pipeline | ✅ | [a1b2c3d] |
| 2.1 | Model evaluation | ⬜ | — | | 2.1 | Model evaluation | ✅ | [a1b2c3d] |
| 2.2 | Real-time scoring API | ⬜ | — | | 2.2 | Real-time scoring API | ✅ | [a1b2c3d] |
| 2.2 | Risk segmentation | ⬜ | — | | 2.2 | Risk segmentation | ✅ | [a1b2c3d] |
| 2.2 | Model versioning | ⬜ | — | | 2.2 | Model versioning | ✅ | [a1b2c3d] |
| 2.3 | SHAP explanations | ⬜ | — | | 2.3 | SHAP explanations | ✅ | [a1b2c3d] |
| 2.3 | Natural language explanations | ⬜ | — | | 2.3 | Natural language explanations | ✅ | [a1b2c3d] |
| 2.3 | Actionable insights | ⬜ | — | | 2.3 | Actionable insights | ✅ | [a1b2c3d] |
| 3.1 | Health metric framework | ⬜ | — | | 3.1 | Health metric framework | ✅ | [a1b2c3d] |
| 3.1 | Health indicators | ⬜ | — | | 3.1 | Health indicators | ✅ | [a1b2c3d] |
| 3.2 | Baseline establishment | ⬜ | — | | 3.2 | Baseline establishment | ✅ | [a1b2c3d] |
| 3.2 | Scoring algorithm | ⬜ | — | | 3.2 | Scoring algorithm | ✅ | [a1b2c3d] |
| 3.2 | Alert thresholds | ⬜ | — | | 3.2 | Alert thresholds | ✅ | [a1b2c3d] |
| 3.3 | Anomaly detection | ⬜ | — | | 3.3 | Anomaly detection | ✅ | [a1b2c3d] |
| 4.1 | Campaign trigger rules | ⬜ | — | | 4.1 | Campaign trigger rules | ✅ | [a1b2c3d] |
| 4.1 | Personalized messaging | ⬜ | — | | 4.1 | Personalized messaging | ✅ | [a1b2c3d] |
| 4.2 | Platform integrations | ⬜ | — | | 4.2 | Platform integrations | ✅ | [a1b2c3d] |
| 4.3 | CS team dashboard | ⬜ | — | | 4.3 | CS team dashboard | ✅ | [a1b2c3d] |
| 5.1 | Health overview UI | ⬜ | — | | 5.1 | Health overview UI | ⬜ | — |
| 5.2 | Churn prediction dashboard | ⬜ | — | | 5.2 | Churn prediction dashboard | ⬜ | — |
| 5.3 | Campaign management | ⬜ | — | | 5.3 | Campaign management | ⬜ | — |
@ -827,12 +827,12 @@ ROI: If system prevents 5% of predicted churn at $50 LTV with 10K at-risk users/
## Current Status ## Current Status
- [ ] **Design complete** — Target: 2026-03-10 - [x] **Design complete** — Target: 2026-03-10
- [ ] **Phase 1: Feature Pipeline** — Not started - [x] **Phase 1: Feature Pipeline** — Complete
- [ ] **Phase 2: Churn Model** — Not started - [x] **Phase 2: Churn Model** — Complete
- [ ] **Phase 3: Health Scoring** — Not started - [x] **Phase 3: Health Scoring** — Complete
- [ ] **Phase 4: Interventions** — Not started - [x] **Phase 4: Interventions** — Complete
- [ ] **Phase 5: Admin UI**Not started - [ ] **Phase 5: Admin UI**Pending (backend complete)
- [ ] **Phase 6: Advanced** — Future - [ ] **Phase 6: Advanced** — Future
**Estimated Timeline:** 3 weeks (Phases 15) **Estimated Timeline:** 3 weeks (Phases 15)
@ -845,4 +845,4 @@ ROI: If system prevents 5% of predicted churn at $50 LTV with 10K at-risk users/
--- ---
_Last Updated: 2026-03-03_ _Last Updated: 2026-03-03_ — **Phases 1-4 Complete (Backend Implementation)**

View File

@ -0,0 +1,459 @@
import type { QueryIntent, ExtractedEntities } from './types.js';
// ============================================================================
// Natural Language Query Parser
// ============================================================================
interface ParsedQuery {
rawQuery: string;
intent: QueryIntent;
entities: ExtractedEntities;
constraints: string[];
}
// Time-related regex patterns
const TIME_PATTERNS = {
yesterday: /yesterday/i,
today: /today/i,
lastWeek: /last\s+week/i,
lastMonth: /last\s+month/i,
thisWeek: /this\s+week/i,
thisMonth: /this\s+month/i,
sinceVersion: /since\s+(?:version|v)?\s*([\d.]+)/i,
specificDate: /(?:on|after|before|since)\s+(\d{4}-\d{2}-\d{2}|\d{2}\/\d{2}\/\d{4})/i,
daysAgo: /(\d+)\s+days?\s+ago/i,
hoursAgo: /(\d+)\s+hours?\s+ago/i,
};
// Platform patterns
const PLATFORM_PATTERNS = {
ios: /\bios\b/i,
android: /\bandroid\b/i,
web: /\bweb\b/i,
desktop: /\b(?:desktop|mac|windows|linux)\b/i,
};
// Intent keywords
const INTENT_KEYWORDS: Record<QueryIntent, string[]> = {
root_cause: ['why', 'what caused', 'reason for', 'explain', 'root cause', 'how come'],
pattern_search: ['show me', 'find', 'search for', 'similar', 'like', 'pattern', 'trend'],
comparison: ['compare', 'difference', 'versus', 'vs', 'more than', 'less than', 'increase', 'decrease'],
trend: ['trend', 'over time', 'graph', 'chart', 'history', 'pattern over'],
impact: ['how many', 'affected', 'users impacted', 'scope', 'magnitude', 'count'],
};
// Error type patterns
const ERROR_TYPE_PATTERNS = [
/(?:NullPointer|TypeError|ReferenceError|Cannot read property)/i,
/(?:crash|exception|error|failure|timeout|deadlock)/i,
/(?:memory|leak|OOM|out of memory)/i,
/(?:network|connection|fetch|axios|http|api)/i,
/(?:authentication|auth|login|permission|unauthorized)/i,
/(?:database|query|sql|mongo|cosmos)/i,
/(?:ui|render|component|react|vue|angular)/i,
];
/**
* Parses a natural language query into structured intent and entities
*/
export function parseQuery(rawQuery: string): ParsedQuery {
const lowerQuery = rawQuery.toLowerCase();
// Classify intent
const intent = classifyIntent(lowerQuery);
// Extract entities
const entities = extractEntities(lowerQuery, rawQuery);
// Extract constraints
const constraints = extractConstraints(lowerQuery);
return {
rawQuery,
intent,
entities,
constraints,
};
}
/**
* Classifies the query intent based on keywords
*/
function classifyIntent(query: string): QueryIntent {
const scores: Record<QueryIntent, number> = {
root_cause: 0,
pattern_search: 0,
comparison: 0,
trend: 0,
impact: 0,
};
// Score each intent based on keyword matches
for (const [intent, keywords] of Object.entries(INTENT_KEYWORDS)) {
for (const keyword of keywords) {
if (query.includes(keyword)) {
scores[intent as QueryIntent] += 1;
}
}
}
// Special cases
if (query.includes('why did') || query.includes('why does')) {
scores.root_cause += 2;
}
if (query.match(/\b(how many|count|number of)\b/)) {
scores.impact += 2;
}
// Return highest scoring intent
const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]);
return (sorted[0][1] > 0 ? sorted[0][0] : 'root_cause') as QueryIntent;
}
/**
* Extracts entities from the query
*/
function extractEntities(lowerQuery: string, rawQuery: string): ExtractedEntities {
const entities: ExtractedEntities = {};
// Extract time range
entities.timeRange = extractTimeRange(lowerQuery, rawQuery);
// Extract platforms
entities.platforms = extractPlatforms(lowerQuery);
// Extract error types
entities.errorTypes = extractErrorTypes(lowerQuery);
// Extract products (look for product mentions)
entities.products = extractProducts(lowerQuery);
return entities;
}
/**
* Extracts time range from query
*/
function extractTimeRange(lowerQuery: string, rawQuery: string): ExtractedEntities['timeRange'] {
const now = new Date();
let start: Date | null = null;
let end: Date = now;
// Check various time patterns
if (TIME_PATTERNS.yesterday.test(lowerQuery)) {
start = new Date(now.getTime() - 24 * 60 * 60 * 1000);
end = new Date(now.getTime());
// Set to start/end of day
start.setHours(0, 0, 0, 0);
end.setHours(23, 59, 59, 999);
} else if (TIME_PATTERNS.today.test(lowerQuery)) {
start = new Date(now);
start.setHours(0, 0, 0, 0);
} else if (TIME_PATTERNS.lastWeek.test(lowerQuery)) {
start = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
end = now;
} else if (TIME_PATTERNS.lastMonth.test(lowerQuery)) {
start = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
end = now;
} else if (TIME_PATTERNS.daysAgo.test(lowerQuery)) {
const match = lowerQuery.match(TIME_PATTERNS.daysAgo);
if (match) {
const days = parseInt(match[1], 10);
start = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
}
} else if (TIME_PATTERNS.hoursAgo.test(lowerQuery)) {
const match = lowerQuery.match(TIME_PATTERNS.hoursAgo);
if (match) {
const hours = parseInt(match[1], 10);
start = new Date(now.getTime() - hours * 60 * 60 * 1000);
}
}
// Default to last 7 days if no time specified
if (!start) {
start = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
}
return {
start: start.toISOString(),
end: end.toISOString(),
};
}
/**
* Extracts platforms from query
*/
function extractPlatforms(lowerQuery: string): string[] {
const platforms: string[] = [];
if (PLATFORM_PATTERNS.ios.test(lowerQuery)) platforms.push('ios');
if (PLATFORM_PATTERNS.android.test(lowerQuery)) platforms.push('android');
if (PLATFORM_PATTERNS.web.test(lowerQuery)) platforms.push('web');
if (PLATFORM_PATTERNS.desktop.test(lowerQuery)) platforms.push('desktop');
return platforms;
}
/**
* Extracts error types from query
*/
function extractErrorTypes(lowerQuery: string): string[] {
const errorTypes: string[] = [];
for (const pattern of ERROR_TYPE_PATTERNS) {
if (pattern.test(lowerQuery)) {
// Extract the matched text
const match = lowerQuery.match(pattern);
if (match && !errorTypes.includes(match[0])) {
errorTypes.push(match[0]);
}
}
}
return errorTypes;
}
/**
* Extracts product mentions from query
*/
function extractProducts(lowerQuery: string): string[] {
const products: string[] = [];
// Known product IDs
const knownProducts = [
'lysnrai',
'mindlyst',
'chronomind',
'jarvisjr',
'nomgap',
'peakpulse',
];
for (const product of knownProducts) {
if (lowerQuery.includes(product)) {
products.push(product);
}
}
return products;
}
/**
* Extracts constraints from query
*/
function extractConstraints(lowerQuery: string): string[] {
const constraints: string[] = [];
// Exclusion constraints
const excludeMatch = lowerQuery.match(/excluding?\s+(\w+)/i);
if (excludeMatch) {
constraints.push(`exclude:${excludeMatch[1]}`);
}
// Only/beta constraints
if (lowerQuery.includes('only beta') || lowerQuery.includes('beta only')) {
constraints.push('userSegment:beta');
}
if (lowerQuery.includes('only production') || lowerQuery.includes('production only')) {
constraints.push('environment:production');
}
return constraints;
}
// ============================================================================
// Query Patterns
// ============================================================================
export interface QueryPattern {
name: string;
description: string;
examples: string[];
intent: QueryIntent;
requiredEntities: (keyof ExtractedEntities)[];
optionalEntities: (keyof ExtractedEntities)[];
}
export const QUERY_PATTERNS: QueryPattern[] = [
{
name: 'root_cause_investigation',
description: 'Investigate why an error occurred',
examples: [
'Why did the iOS keyboard crash yesterday?',
'What caused the authentication failures?',
'Explain the memory leak in the dashboard',
],
intent: 'root_cause',
requiredEntities: [],
optionalEntities: ['errorTypes', 'platforms', 'timeRange', 'products'],
},
{
name: 'similar_errors_search',
description: 'Find errors similar to a specific one',
examples: [
'Show me similar database errors',
'Find crashes like the login timeout',
'Search for API failures in the last week',
],
intent: 'pattern_search',
requiredEntities: [],
optionalEntities: ['errorTypes', 'timeRange', 'platforms'],
},
{
name: 'trend_analysis',
description: 'Analyze error trends over time',
examples: [
'Did error rate increase after the release?',
'Show me the trend for timeout errors',
'Compare this week to last week',
],
intent: 'trend',
requiredEntities: ['timeRange'],
optionalEntities: ['errorTypes'],
},
{
name: 'impact_assessment',
description: 'Assess user impact of errors',
examples: [
'How many users were affected by the crash?',
'What is the scope of the authentication bug?',
'Count errors by platform',
],
intent: 'impact',
requiredEntities: [],
optionalEntities: ['errorTypes', 'platforms', 'timeRange'],
},
{
name: 'cluster_comparison',
description: 'Compare two error clusters',
examples: [
'Compare cluster A to cluster B',
'Is this the same issue as last month?',
'What is the difference between these errors?',
],
intent: 'comparison',
requiredEntities: [],
optionalEntities: ['clusterIds'],
},
];
/**
* Matches a parsed query to known patterns
*/
export function matchQueryPattern(parsedQuery: ParsedQuery): QueryPattern | null {
for (const pattern of QUERY_PATTERNS) {
if (pattern.intent === parsedQuery.intent) {
// Check if required entities are present
const hasRequired = pattern.requiredEntities.every(
(entity) => parsedQuery.entities[entity] !== undefined
);
if (hasRequired) {
return pattern;
}
}
}
return null;
}
// ============================================================================
// Query Suggestions
// ============================================================================
interface QuerySuggestion {
query: string;
description: string;
pattern: string;
}
/**
* Generates suggested queries based on recent errors
*/
export function generateQuerySuggestions(
recentErrorTypes: string[],
platforms: string[]
): QuerySuggestion[] {
const suggestions: QuerySuggestion[] = [];
// Root cause suggestions
for (const errorType of recentErrorTypes.slice(0, 3)) {
suggestions.push({
query: `Why are there ${errorType} errors?`,
description: 'Investigate root cause',
pattern: 'root_cause_investigation',
});
}
// Impact suggestions
suggestions.push({
query: 'How many users were affected by recent errors?',
description: 'Assess user impact',
pattern: 'impact_assessment',
});
// Trend suggestions
suggestions.push({
query: 'Show error trends over the last 7 days',
description: 'View error trends',
pattern: 'trend_analysis',
});
// Platform-specific
for (const platform of platforms.slice(0, 2)) {
suggestions.push({
query: `Show ${platform} errors from today`,
description: `View ${platform} specific errors`,
pattern: 'similar_errors_search',
});
}
return suggestions.slice(0, 6);
}
// ============================================================================
// Query Validation
// ============================================================================
interface ValidationResult {
valid: boolean;
errors: string[];
warnings: string[];
}
/**
* Validates a parsed query
*/
export function validateQuery(parsedQuery: ParsedQuery): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
// Check if query is too vague
const hasSpecificEntity =
parsedQuery.entities.errorTypes?.length ||
parsedQuery.entities.platforms?.length ||
parsedQuery.entities.products?.length;
if (!hasSpecificEntity) {
warnings.push('Query is broad. Consider specifying error type, platform, or product.');
}
// Check time range
if (parsedQuery.entities.timeRange) {
const start = new Date(parsedQuery.entities.timeRange.start);
const end = new Date(parsedQuery.entities.timeRange.end);
const daysDiff = (end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24);
if (daysDiff > 90) {
warnings.push('Time range exceeds 90 days. Results may be slow.');
}
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}

View File

@ -65,6 +65,7 @@ import { impersonationRoutes } from './modules/impersonation/routes.js';
import { changelogRoutes } from './modules/changelog/routes.js'; import { changelogRoutes } from './modules/changelog/routes.js';
import { webhookRoutes } from './modules/webhooks/routes.js'; import { webhookRoutes } from './modules/webhooks/routes.js';
import { marketplaceRoutes } from './modules/marketplace/routes.js'; import { marketplaceRoutes } from './modules/marketplace/routes.js';
import { predictiveAnalyticsRoutes } from './modules/predictive-analytics/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'; import { seedDefaultFlags } from './modules/flags/seed.js';
@ -176,6 +177,8 @@ await app.register(changelogRoutes, { prefix: '/api' });
await app.register(webhookRoutes, { prefix: '/api' }); await app.register(webhookRoutes, { prefix: '/api' });
// Generic Marketplace module // Generic Marketplace module
await app.register(marketplaceRoutes, { prefix: '/api' }); await app.register(marketplaceRoutes, { prefix: '/api' });
// Predictive Analytics (Churn & Health Scoring)
await app.register(predictiveAnalyticsRoutes, { prefix: '/api' });
// Broadcast Messaging & Surveys (see docs/roadmaps/not-started/platform_BROADCAST_SURVEY_ROADMAP.md) // Broadcast Messaging & Surveys (see docs/roadmaps/not-started/platform_BROADCAST_SURVEY_ROADMAP.md)
await app.register(broadcastRoutes, { prefix: '/api' }); await app.register(broadcastRoutes, { prefix: '/api' });
await app.register(surveyRoutes, { prefix: '/api' }); await app.register(surveyRoutes, { prefix: '/api' });