widgettdc-api / apps /matrix-frontend /src /services /SourceWidgetDiscovery.ts
Kraft102's picture
fix: sql.js Docker/Alpine compatibility layer for PatternMemory and FailureMemory
5a81b95
/**
* SourceWidgetDiscovery - Bidirectional Source↔Widget Coupling
*
* This service handles:
* 1. Widget → Source: Widgets request data, suggests additional sources
* 2. Source → Widget: Discovers orphan sources and suggests/generates widgets
*
* The autonomous system learns from these connections over time.
*/
import { useState, useEffect, useCallback } from 'react';
const API_BASE = 'http://localhost:3001/api';
// ============================================================
// TYPES
// ============================================================
export interface DiscoveredSource {
id: string;
name: string;
type: string;
capabilities: string[];
estimatedLatency: number;
recommendedWidgets: string[];
hasWidget: boolean;
suggestNewWidget: boolean;
}
export interface SuggestedWidget {
forSource: string;
suggestedName: string;
suggestedType: string;
capabilities: string[];
}
export interface GeneratedWidget {
id: string;
name: string;
category: string;
dataSource: string;
capabilities: string[];
config: Array<{ key: string; type: string; default: any; label: string }>;
template: 'data-display' | 'chart' | 'table' | 'status';
generatedAt: string;
}
export interface SourceDiscoveryResult {
total: number;
sources: DiscoveredSource[];
orphanSources: DiscoveredSource[];
suggestedNewWidgets: SuggestedWidget[];
}
// ============================================================
// SOURCE WIDGET DISCOVERY SERVICE
// ============================================================
class SourceWidgetDiscoveryService {
private wsConnection: WebSocket | null = null;
private listeners: Map<string, Set<(data: any) => void>> = new Map();
private discoveryCache: SourceDiscoveryResult | null = null;
private cacheTimestamp: number = 0;
private readonly CACHE_TTL = 60000; // 1 minute
/**
* Fetch all discovered sources and their widget recommendations
*/
async discoverSources(): Promise<SourceDiscoveryResult> {
// Return cache if fresh
if (this.discoveryCache && Date.now() - this.cacheTimestamp < this.CACHE_TTL) {
return this.discoveryCache;
}
try {
const response = await fetch(`${API_BASE}/acquisition/discovered-sources`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await response.json();
if (result.success) {
this.discoveryCache = result.data;
this.cacheTimestamp = Date.now();
return result.data;
}
throw new Error(result.error || 'Failed to discover sources');
} catch (error) {
console.error('[SourceWidgetDiscovery] Error:', error);
// Return empty result on error
return {
total: 0,
sources: [],
orphanSources: [],
suggestedNewWidgets: [],
};
}
}
/**
* Request widget generation for an orphan source
*/
async generateWidgetForSource(
sourceId: string,
widgetName?: string,
widgetType?: string
): Promise<GeneratedWidget | null> {
try {
const response = await fetch(`${API_BASE}/acquisition/generate-widget`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sourceId, widgetName, widgetType }),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await response.json();
if (result.success) {
// Notify listeners of new widget
this.emit('widget-generated', result.widget);
return result.widget;
}
return null;
} catch (error) {
console.error('[SourceWidgetDiscovery] Widget generation error:', error);
return null;
}
}
/**
* Enable a data source (triggered by user from widget recommendation)
*/
async enableSource(
sourceId: string,
sourceName: string,
category: string,
requiresApproval: boolean = false
): Promise<boolean> {
try {
const response = await fetch(`${API_BASE}/acquisition/enable-source`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sourceId, sourceName, category, requiresApproval }),
});
if (!response.ok) return false;
const result = await response.json();
if (result.success) {
// Invalidate cache
this.discoveryCache = null;
// Notify listeners
this.emit('source-enabled', { sourceId, sourceName, category });
return true;
}
return false;
} catch (error) {
console.error('[SourceWidgetDiscovery] Enable source error:', error);
return false;
}
}
/**
* Find the best widget for a given source
*/
findWidgetForSource(source: DiscoveredSource): string {
// Priority mapping
const typeToWidget: Record<string, string> = {
'database': 'database',
'mcp-tool': 'events',
'email-adapter': 'news',
'file': 'datastreams',
'external': 'global',
'osint': 'threatmap',
'threatIntel': 'alerts',
};
// Check capabilities for more specific matches
const caps = source.capabilities.join(' ').toLowerCase();
if (caps.includes('alert') || caps.includes('security')) return 'alerts';
if (caps.includes('metric') || caps.includes('stats')) return 'metrics';
if (caps.includes('event') || caps.includes('log')) return 'events';
if (caps.includes('graph') || caps.includes('knowledge')) return 'statistics';
if (caps.includes('geo') || caps.includes('location')) return 'threatmap';
return typeToWidget[source.type] || 'events';
}
/**
* Get sources that need widgets (orphans)
*/
async getOrphanSources(): Promise<DiscoveredSource[]> {
const discovery = await this.discoverSources();
return discovery.orphanSources;
}
/**
* Subscribe to WebSocket events for real-time source discovery
*/
connectWebSocket(): void {
if (this.wsConnection?.readyState === WebSocket.OPEN) return;
try {
this.wsConnection = new WebSocket('ws://localhost:3001/mcp/ws');
this.wsConnection.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
// Handle source:health events (new sources discovered)
if (data.type === 'source:health') {
this.emit('source-discovered', data);
// Invalidate cache to pick up new source
this.discoveryCache = null;
}
// Handle widget:invoke events (widget generation)
if (data.type === 'widget:invoke' && data.action === 'widget-generated') {
this.emit('widget-generated', data.widget);
}
} catch (e) {
// Ignore parse errors
}
};
this.wsConnection.onclose = () => {
// Reconnect after delay
setTimeout(() => this.connectWebSocket(), 5000);
};
} catch (error) {
console.error('[SourceWidgetDiscovery] WebSocket error:', error);
}
}
/**
* Event emitter pattern
*/
on(event: string, callback: (data: any) => void): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(callback);
}
off(event: string, callback: (data: any) => void): void {
this.listeners.get(event)?.delete(callback);
}
private emit(event: string, data: any): void {
this.listeners.get(event)?.forEach(cb => cb(data));
}
/**
* Cleanup
*/
disconnect(): void {
if (this.wsConnection) {
this.wsConnection.close();
this.wsConnection = null;
}
this.listeners.clear();
}
}
// Singleton instance
export const sourceWidgetDiscovery = new SourceWidgetDiscoveryService();
// ============================================================
// REACT HOOKS
// ============================================================
/**
* Hook to discover orphan sources and suggest widgets
*/
export function useSourceDiscovery() {
const [sources, setSources] = useState<DiscoveredSource[]>([]);
const [orphanSources, setOrphanSources] = useState<DiscoveredSource[]>([]);
const [suggestedWidgets, setSuggestedWidgets] = useState<SuggestedWidget[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const refresh = useCallback(async () => {
setIsLoading(true);
try {
const discovery = await sourceWidgetDiscovery.discoverSources();
setSources(discovery.sources);
setOrphanSources(discovery.orphanSources);
setSuggestedWidgets(discovery.suggestedNewWidgets);
setError(null);
} catch (err: any) {
setError(err.message);
} finally {
setIsLoading(false);
}
}, []);
const generateWidget = useCallback(async (sourceId: string, name?: string) => {
const widget = await sourceWidgetDiscovery.generateWidgetForSource(sourceId, name);
if (widget) {
// Refresh to update orphan list
await refresh();
}
return widget;
}, [refresh]);
useEffect(() => {
refresh();
// Connect to WebSocket for real-time updates
sourceWidgetDiscovery.connectWebSocket();
const handleNewSource = () => refresh();
sourceWidgetDiscovery.on('source-discovered', handleNewSource);
sourceWidgetDiscovery.on('source-enabled', handleNewSource);
return () => {
sourceWidgetDiscovery.off('source-discovered', handleNewSource);
sourceWidgetDiscovery.off('source-enabled', handleNewSource);
};
}, [refresh]);
return {
sources,
orphanSources,
suggestedWidgets,
isLoading,
error,
refresh,
generateWidget,
enableSource: sourceWidgetDiscovery.enableSource.bind(sourceWidgetDiscovery),
};
}
/**
* Hook to listen for newly generated widgets
*/
export function useGeneratedWidgets() {
const [generatedWidgets, setGeneratedWidgets] = useState<GeneratedWidget[]>([]);
useEffect(() => {
sourceWidgetDiscovery.connectWebSocket();
const handleWidget = (widget: GeneratedWidget) => {
setGeneratedWidgets(prev => [...prev, widget]);
};
sourceWidgetDiscovery.on('widget-generated', handleWidget);
return () => {
sourceWidgetDiscovery.off('widget-generated', handleWidget);
};
}, []);
return {
generatedWidgets,
clearWidgets: () => setGeneratedWidgets([]),
};
}