File size: 4,721 Bytes
7b4f5dd
 
 
 
 
 
 
 
a1d2eec
7b4f5dd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/* ═══════════════════════════════════════════════════════════════
   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();
}