431 lines
13 KiB
TypeScript
431 lines
13 KiB
TypeScript
import { contentPlanningApi } from './contentPlanningApi';
|
|
|
|
export interface ServiceStatus {
|
|
name: string;
|
|
status: 'loading' | 'success' | 'error' | 'idle';
|
|
progress: number;
|
|
message: string;
|
|
data?: any;
|
|
error?: string;
|
|
}
|
|
|
|
export interface DashboardData {
|
|
strategies: any[];
|
|
gapAnalyses: any[];
|
|
aiInsights: any[];
|
|
aiRecommendations: any[];
|
|
calendarEvents: any[];
|
|
healthStatus: {
|
|
backend: boolean;
|
|
database: boolean;
|
|
aiServices: boolean;
|
|
};
|
|
}
|
|
|
|
export class ContentPlanningOrchestrator {
|
|
private serviceStatuses: Map<string, ServiceStatus> = new Map();
|
|
private onProgressUpdate?: (statuses: ServiceStatus[]) => void;
|
|
private onDataUpdate?: (data: Partial<DashboardData>) => void;
|
|
private latestDashboardData: DashboardData | null = null;
|
|
|
|
constructor() {
|
|
this.initializeServiceStatuses();
|
|
}
|
|
|
|
private initializeServiceStatuses() {
|
|
const services = [
|
|
{ name: 'strategies', displayName: 'Content Strategies' },
|
|
{ name: 'gapAnalyses', displayName: 'Gap Analysis' },
|
|
{ name: 'aiAnalytics', displayName: 'AI Analytics' },
|
|
{ name: 'calendarEvents', displayName: 'Calendar Events' },
|
|
{ name: 'healthCheck', displayName: 'System Health' }
|
|
];
|
|
|
|
services.forEach(service => {
|
|
this.serviceStatuses.set(service.name, {
|
|
name: service.displayName,
|
|
status: 'idle',
|
|
progress: 0,
|
|
message: 'Ready to load'
|
|
});
|
|
});
|
|
}
|
|
|
|
public setProgressCallback(callback: (statuses: ServiceStatus[]) => void) {
|
|
this.onProgressUpdate = callback;
|
|
}
|
|
|
|
public setDataUpdateCallback(callback: (data: Partial<DashboardData>) => void) {
|
|
this.onDataUpdate = callback;
|
|
}
|
|
|
|
private updateServiceStatus(name: string, updates: Partial<ServiceStatus>) {
|
|
const current = this.serviceStatuses.get(name);
|
|
if (current) {
|
|
const updated = { ...current, ...updates };
|
|
this.serviceStatuses.set(name, updated);
|
|
this.notifyProgressUpdate();
|
|
}
|
|
}
|
|
|
|
private notifyProgressUpdate() {
|
|
if (this.onProgressUpdate) {
|
|
this.onProgressUpdate(Array.from(this.serviceStatuses.values()));
|
|
}
|
|
}
|
|
|
|
private notifyDataUpdate(data: Partial<DashboardData>) {
|
|
if (this.onDataUpdate) {
|
|
this.onDataUpdate(data);
|
|
}
|
|
}
|
|
|
|
public async loadDashboardData(): Promise<DashboardData> {
|
|
// Reset all service statuses
|
|
this.serviceStatuses.forEach((status, name) => {
|
|
this.updateServiceStatus(name, {
|
|
status: 'loading',
|
|
progress: 0,
|
|
message: 'Initializing...'
|
|
});
|
|
});
|
|
|
|
// Start parallel requests
|
|
const promises = [
|
|
this.loadStrategies(),
|
|
this.loadGapAnalyses(),
|
|
this.loadAIAnalytics(),
|
|
this.loadCalendarEvents(),
|
|
this.loadHealthStatus()
|
|
];
|
|
|
|
// Wait for all to complete but handle each independently
|
|
const results = await Promise.allSettled(promises);
|
|
|
|
// Compile final data
|
|
const dashboardData: DashboardData = {
|
|
strategies: [],
|
|
gapAnalyses: [],
|
|
aiInsights: [],
|
|
aiRecommendations: [],
|
|
calendarEvents: [],
|
|
healthStatus: {
|
|
backend: false,
|
|
database: false,
|
|
aiServices: false
|
|
}
|
|
};
|
|
|
|
results.forEach((result) => {
|
|
if (result.status === 'fulfilled') {
|
|
const data = result.value;
|
|
// Type-safe data assignment
|
|
if ('strategies' in data) dashboardData.strategies = data.strategies;
|
|
if ('gapAnalyses' in data) dashboardData.gapAnalyses = data.gapAnalyses;
|
|
if ('aiInsights' in data) dashboardData.aiInsights = data.aiInsights;
|
|
if ('aiRecommendations' in data) dashboardData.aiRecommendations = data.aiRecommendations;
|
|
if ('calendarEvents' in data) dashboardData.calendarEvents = data.calendarEvents;
|
|
if ('healthStatus' in data) dashboardData.healthStatus = data.healthStatus;
|
|
}
|
|
});
|
|
|
|
this.latestDashboardData = dashboardData;
|
|
return dashboardData;
|
|
}
|
|
|
|
private async loadStrategies() {
|
|
try {
|
|
this.updateServiceStatus('strategies', {
|
|
status: 'loading',
|
|
progress: 10,
|
|
message: 'Loading content strategies...'
|
|
});
|
|
|
|
const strategies = await contentPlanningApi.getStrategiesSafe();
|
|
|
|
this.updateServiceStatus('strategies', {
|
|
status: 'loading',
|
|
progress: 50,
|
|
message: 'Processing strategy data...'
|
|
});
|
|
|
|
// Simulate processing time for better UX
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
this.updateServiceStatus('strategies', {
|
|
status: 'success',
|
|
progress: 100,
|
|
message: `Loaded ${strategies.length} content strategies`,
|
|
data: strategies
|
|
});
|
|
|
|
this.notifyDataUpdate({ strategies });
|
|
|
|
return { strategies };
|
|
} catch (error: any) {
|
|
this.updateServiceStatus('strategies', {
|
|
status: 'error',
|
|
progress: 0,
|
|
message: 'Failed to load strategies',
|
|
error: error.message
|
|
});
|
|
return { strategies: [] };
|
|
}
|
|
}
|
|
|
|
private async loadGapAnalyses() {
|
|
try {
|
|
this.updateServiceStatus('gapAnalyses', {
|
|
status: 'loading',
|
|
progress: 10,
|
|
message: 'Initializing gap analysis...'
|
|
});
|
|
|
|
const response = await contentPlanningApi.getGapAnalysesSafe();
|
|
|
|
this.updateServiceStatus('gapAnalyses', {
|
|
status: 'loading',
|
|
progress: 30,
|
|
message: 'Analyzing content gaps...'
|
|
});
|
|
|
|
// Simulate processing time
|
|
await new Promise(resolve => setTimeout(resolve, 800));
|
|
|
|
this.updateServiceStatus('gapAnalyses', {
|
|
status: 'loading',
|
|
progress: 70,
|
|
message: 'Processing gap analysis results...'
|
|
});
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
this.updateServiceStatus('gapAnalyses', {
|
|
status: 'success',
|
|
progress: 100,
|
|
message: `Found ${response.gap_analyses?.length || 0} content gaps`,
|
|
data: response
|
|
});
|
|
|
|
this.notifyDataUpdate({ gapAnalyses: response.gap_analyses || [] });
|
|
|
|
return { gapAnalyses: response.gap_analyses || [] };
|
|
} catch (error: any) {
|
|
this.updateServiceStatus('gapAnalyses', {
|
|
status: 'error',
|
|
progress: 0,
|
|
message: 'Failed to load gap analysis',
|
|
error: error.message
|
|
});
|
|
return { gapAnalyses: [] };
|
|
}
|
|
}
|
|
|
|
private async loadAIAnalytics() {
|
|
try {
|
|
this.updateServiceStatus('aiAnalytics', {
|
|
status: 'loading',
|
|
progress: 10,
|
|
message: 'Initializing AI analysis...'
|
|
});
|
|
|
|
// New approach: stream strategic intelligence data and show status from AI generation SSE
|
|
return await new Promise<{ aiInsights: any[]; aiRecommendations: any[] }>(async (resolve) => {
|
|
// 1) Execution status stream (best-effort; ignore if no active strategy)
|
|
try {
|
|
const currentStrategyId = this.latestDashboardData?.strategies?.[0]?.id;
|
|
if (currentStrategyId) {
|
|
const statusSource = await contentPlanningApi.streamAIGenerationStatus(currentStrategyId);
|
|
statusSource.onmessage = (event: MessageEvent) => {
|
|
try {
|
|
const data = JSON.parse(event.data);
|
|
if (data.type === 'progress') {
|
|
this.updateServiceStatus('aiAnalytics', {
|
|
status: 'loading',
|
|
progress: Math.min(99, data.progress || 20),
|
|
message: data.detail || 'AI generation in progress...'
|
|
});
|
|
}
|
|
if (data.type === 'result') {
|
|
this.updateServiceStatus('aiAnalytics', {
|
|
status: data.status === 'completed' ? 'success' : 'error',
|
|
progress: 100,
|
|
message: data.status === 'completed' ? 'AI generation completed' : 'AI generation failed'
|
|
});
|
|
statusSource.close();
|
|
}
|
|
} catch {}
|
|
};
|
|
statusSource.onerror = () => statusSource.close();
|
|
}
|
|
} catch {}
|
|
|
|
// 2) Data stream for insights (Strategic Intelligence)
|
|
const intelSource = await contentPlanningApi.streamStrategicIntelligence(1);
|
|
contentPlanningApi.handleSSEData(
|
|
intelSource,
|
|
(data) => {
|
|
if (data.type === 'progress') {
|
|
this.updateServiceStatus('aiAnalytics', {
|
|
status: 'loading',
|
|
progress: Math.max(20, data.progress || 40),
|
|
message: data.message || 'Analyzing strategic intelligence...'
|
|
});
|
|
} else if (data.type === 'result' && data.status === 'success') {
|
|
this.updateServiceStatus('aiAnalytics', {
|
|
status: 'success',
|
|
progress: 100,
|
|
message: 'Strategic intelligence ready',
|
|
data: data.data
|
|
});
|
|
// Map to orchestrator fields if needed
|
|
this.notifyDataUpdate({ aiInsights: data.data?.recommendations || [], aiRecommendations: [] });
|
|
resolve({ aiInsights: data.data?.recommendations || [], aiRecommendations: [] });
|
|
} else if (data.type === 'error') {
|
|
this.updateServiceStatus('aiAnalytics', {
|
|
status: 'error',
|
|
progress: 0,
|
|
message: data.message || 'Failed to load strategic intelligence'
|
|
});
|
|
resolve({ aiInsights: [], aiRecommendations: [] });
|
|
}
|
|
},
|
|
() => {
|
|
resolve({ aiInsights: [], aiRecommendations: [] });
|
|
}
|
|
);
|
|
});
|
|
} catch (error: any) {
|
|
this.updateServiceStatus('aiAnalytics', {
|
|
status: 'error',
|
|
progress: 0,
|
|
message: 'Failed to load AI analytics',
|
|
error: error.message
|
|
});
|
|
return { aiInsights: [], aiRecommendations: [] };
|
|
}
|
|
}
|
|
|
|
private async loadCalendarEvents() {
|
|
try {
|
|
this.updateServiceStatus('calendarEvents', {
|
|
status: 'loading',
|
|
progress: 10,
|
|
message: 'Loading calendar events...'
|
|
});
|
|
|
|
const calendarEvents = await contentPlanningApi.getEventsSafe();
|
|
|
|
this.updateServiceStatus('calendarEvents', {
|
|
status: 'loading',
|
|
progress: 50,
|
|
message: 'Processing calendar data...'
|
|
});
|
|
|
|
// Simulate processing time
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
|
|
this.updateServiceStatus('calendarEvents', {
|
|
status: 'success',
|
|
progress: 100,
|
|
message: `Loaded ${calendarEvents.length} calendar events`,
|
|
data: calendarEvents
|
|
});
|
|
|
|
this.notifyDataUpdate({ calendarEvents });
|
|
|
|
return { calendarEvents };
|
|
} catch (error: any) {
|
|
this.updateServiceStatus('calendarEvents', {
|
|
status: 'error',
|
|
progress: 0,
|
|
message: 'Failed to load calendar events',
|
|
error: error.message
|
|
});
|
|
return { calendarEvents: [] };
|
|
}
|
|
}
|
|
|
|
private async loadHealthStatus() {
|
|
try {
|
|
this.updateServiceStatus('healthCheck', {
|
|
status: 'loading',
|
|
progress: 25,
|
|
message: 'Checking system health...'
|
|
});
|
|
|
|
const [backendHealth, aiHealth] = await Promise.allSettled([
|
|
contentPlanningApi.checkBackendHealth(),
|
|
contentPlanningApi.checkAIHealth()
|
|
]);
|
|
|
|
const healthStatus = {
|
|
backend: backendHealth.status === 'fulfilled' && backendHealth.value.status === 'healthy',
|
|
database: backendHealth.status === 'fulfilled' && backendHealth.value.services?.database_connection === true,
|
|
aiServices: aiHealth.status === 'fulfilled' && aiHealth.value.status === 'healthy'
|
|
};
|
|
|
|
this.updateServiceStatus('healthCheck', {
|
|
status: 'success',
|
|
progress: 100,
|
|
message: 'System health check complete',
|
|
data: healthStatus
|
|
});
|
|
|
|
this.notifyDataUpdate({ healthStatus });
|
|
|
|
return { healthStatus };
|
|
} catch (error: any) {
|
|
this.updateServiceStatus('healthCheck', {
|
|
status: 'error',
|
|
progress: 0,
|
|
message: 'Health check failed',
|
|
error: error.message
|
|
});
|
|
return {
|
|
healthStatus: {
|
|
backend: false,
|
|
database: false,
|
|
aiServices: false
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
public getServiceStatuses(): ServiceStatus[] {
|
|
return Array.from(this.serviceStatuses.values());
|
|
}
|
|
|
|
public refreshService(serviceName: string) {
|
|
const status = this.serviceStatuses.get(serviceName);
|
|
if (status) {
|
|
this.updateServiceStatus(serviceName, {
|
|
status: 'loading',
|
|
progress: 0,
|
|
message: 'Refreshing...'
|
|
});
|
|
|
|
// Re-run the specific service
|
|
switch (serviceName) {
|
|
case 'strategies':
|
|
this.loadStrategies();
|
|
break;
|
|
case 'gapAnalyses':
|
|
this.loadGapAnalyses();
|
|
break;
|
|
case 'aiAnalytics':
|
|
this.loadAIAnalytics();
|
|
break;
|
|
case 'calendarEvents':
|
|
this.loadCalendarEvents();
|
|
break;
|
|
case 'healthCheck':
|
|
this.loadHealthStatus();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const contentPlanningOrchestrator = new ContentPlanningOrchestrator();
|