YashashviAlva's picture
πŸ› Fix hardcoded localhost URL for production deployments
a1d2eec
/* ═══════════════════════════════════════════════════════════════
Scan Service β€” Unified interface for mock and live SSE
Automatically switches based on VITE_MOCK_MODE env variable
═══════════════════════════════════════════════════════════════ */
import { createMockService } from './mockService';
const IS_MOCK = import.meta.env.VITE_MOCK_MODE === 'true';
const API_URL = import.meta.env.VITE_API_URL || '';
export class ScanService {
constructor() {
this.mockService = null;
this.eventSource = null;
this.eventHandlers = {};
}
on(eventType, handler) {
if (!this.eventHandlers[eventType]) {
this.eventHandlers[eventType] = [];
}
this.eventHandlers[eventType].push(handler);
return this;
}
emit(eventType, data) {
const handlers = this.eventHandlers[eventType] || [];
handlers.forEach(handler => handler(data));
}
async startScan(payload) {
if (IS_MOCK) {
return this._startMockScan(payload);
}
return this._startLiveScan(payload);
}
async _startMockScan(payload) {
this.mockService = createMockService();
// Forward all events from mock service
const eventTypes = [
'scan_started', 'agent_start', 'finding', 'progress',
'fix_ready', 'complete', 'error', 'event',
'amd_metrics', 'amd_migration_finding', 'amd_migration_summary',
];
eventTypes.forEach(type => {
this.mockService.on(type, (data) => this.emit(type, data));
});
await this.mockService.startScan(payload);
}
async _startLiveScan(payload) {
try {
// POST to initiate scan
const response = await fetch(`${API_URL}/api/scan`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) throw new Error('Scan initiation failed');
const { scanId } = await response.json();
this.emit('scan_started', { scanId });
// Open SSE connection
this.eventSource = new EventSource(`${API_URL}/api/scan/stream/${scanId}`);
this.eventSource.addEventListener('agent_start', (e) => {
this.emit('agent_start', JSON.parse(e.data));
});
this.eventSource.addEventListener('finding', (e) => {
this.emit('finding', JSON.parse(e.data));
});
this.eventSource.addEventListener('progress', (e) => {
this.emit('progress', JSON.parse(e.data));
});
this.eventSource.addEventListener('fix_ready', (e) => {
this.emit('fix_ready', JSON.parse(e.data));
});
this.eventSource.addEventListener('complete', (e) => {
console.log('[ScanService] SSE complete event received');
this.emit('complete', JSON.parse(e.data));
this.eventSource.close();
});
this.eventSource.addEventListener('amd_metrics', (e) => {
this.emit('amd_metrics', JSON.parse(e.data));
});
this.eventSource.addEventListener('amd_migration_finding', (e) => {
this.emit('amd_migration_finding', JSON.parse(e.data));
});
this.eventSource.addEventListener('amd_migration_summary', (e) => {
this.emit('amd_migration_summary', JSON.parse(e.data));
});
// Backend custom SSE 'error' event (different from connection onerror)
this.eventSource.addEventListener('error', (e) => {
// Custom SSE events have a 'data' property; connection errors don't
if (e.data) {
console.log('[ScanService] SSE error event from backend:', e.data);
this.emit('error', JSON.parse(e.data));
this.eventSource.close();
}
});
this.eventSource.onerror = (e) => {
// Only emit connection error if the EventSource is not already closed
if (this.eventSource && this.eventSource.readyState === EventSource.CLOSED) {
return; // Already closed by complete/error handler above
}
this.emit('error', { message: 'Connection to scan server lost' });
this.eventSource.close();
};
} catch (error) {
this.emit('error', { message: error.message });
}
}
stop() {
if (this.mockService) {
this.mockService.stop();
this.mockService = null;
}
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
}
destroy() {
this.stop();
this.eventHandlers = {};
}
}
export function createScanService() {
return new ScanService();
}