JavaScript · 24132 bytes Raw Blame History
1 // LooseCannon Analytics Dashboard JavaScript
2 // Real-time monitoring and control interface
3
4 class LooseCannonDashboard {
5 constructor() {
6 this.serverUrl = 'http://localhost:8765';
7 this.ws = null;
8 this.charts = {};
9 this.updateInterval = null;
10 this.startTime = Date.now();
11 this.data = {
12 messages: [],
13 scammers: [],
14 conversations: new Map(),
15 patterns: [],
16 screenshots: []
17 };
18 this.init();
19 }
20
21 async init() {
22 await this.checkServerConnection();
23 this.initializeCharts();
24 this.setupEventListeners();
25 this.connectWebSocket();
26 this.startRealtimeUpdates();
27 this.loadInitialData();
28 }
29
30 async checkServerConnection() {
31 const statusElement = document.getElementById('serverStatus');
32
33 try {
34 const response = await fetch(`${this.serverUrl}/status`);
35 if (response.ok) {
36 statusElement.classList.add('connected');
37 console.log('Server connected');
38 } else {
39 throw new Error('Server not responding');
40 }
41 } catch (error) {
42 console.error('Server connection failed:', error);
43 statusElement.classList.remove('connected');
44 this.showNotification('Server offline - some features unavailable', 'error');
45 }
46 }
47
48 connectWebSocket() {
49 // WebSocket for real-time updates
50 this.ws = new WebSocket('ws://localhost:8766');
51
52 this.ws.onopen = () => {
53 console.log('WebSocket connected');
54 this.showNotification('Connected to real-time feed', 'success');
55 };
56
57 this.ws.onmessage = (event) => {
58 const data = JSON.parse(event.data);
59 this.handleRealtimeUpdate(data);
60 };
61
62 this.ws.onerror = (error) => {
63 console.error('WebSocket error:', error);
64 };
65
66 this.ws.onclose = () => {
67 console.log('WebSocket disconnected, reconnecting...');
68 setTimeout(() => this.connectWebSocket(), 5000);
69 };
70 }
71
72 handleRealtimeUpdate(data) {
73 switch (data.type) {
74 case 'MESSAGE':
75 this.addActivityItem(data);
76 this.updateMessageStats(data);
77 break;
78
79 case 'SCAMMER_DETECTED':
80 this.handleScammerDetection(data);
81 break;
82
83 case 'SESSION_UPDATE':
84 this.updateActiveSessions(data);
85 break;
86
87 case 'SCREENSHOT':
88 this.addScreenshot(data);
89 break;
90
91 case 'PATTERN_LEARNED':
92 this.addPattern(data);
93 break;
94 }
95
96 this.updateCharts();
97 }
98
99 initializeCharts() {
100 const chartOptions = {
101 responsive: true,
102 maintainAspectRatio: false,
103 plugins: {
104 legend: {
105 labels: {
106 color: '#e0e0e0'
107 }
108 }
109 },
110 scales: {
111 y: {
112 grid: {
113 color: '#333'
114 },
115 ticks: {
116 color: '#888'
117 }
118 },
119 x: {
120 grid: {
121 color: '#333'
122 },
123 ticks: {
124 color: '#888'
125 }
126 }
127 }
128 };
129
130 // Volume Chart
131 this.charts.volume = new Chart(document.getElementById('volumeChart'), {
132 type: 'line',
133 data: {
134 labels: this.generateTimeLabels(24),
135 datasets: [{
136 label: 'Messages',
137 data: new Array(24).fill(0),
138 borderColor: '#667eea',
139 backgroundColor: 'rgba(102, 126, 234, 0.1)',
140 tension: 0.4
141 }]
142 },
143 options: chartOptions
144 });
145
146 // Detection Chart
147 this.charts.detection = new Chart(document.getElementById('detectionChart'), {
148 type: 'bar',
149 data: {
150 labels: ['Low', 'Medium', 'High', 'Critical'],
151 datasets: [{
152 label: 'Detections',
153 data: [0, 0, 0, 0],
154 backgroundColor: ['#10b981', '#f59e0b', '#ef4444', '#7c3aed']
155 }]
156 },
157 options: chartOptions
158 });
159
160 // Platform Chart
161 this.charts.platform = new Chart(document.getElementById('platformChart'), {
162 type: 'doughnut',
163 data: {
164 labels: ['WhatsApp', 'Telegram', 'Messenger'],
165 datasets: [{
166 data: [0, 0, 0],
167 backgroundColor: ['#25d366', '#0088cc', '#0084ff']
168 }]
169 },
170 options: {
171 ...chartOptions,
172 plugins: {
173 legend: {
174 position: 'bottom',
175 labels: {
176 color: '#e0e0e0'
177 }
178 }
179 }
180 }
181 });
182
183 // Response Time Chart
184 this.charts.response = new Chart(document.getElementById('responseChart'), {
185 type: 'line',
186 data: {
187 labels: this.generateTimeLabels(60, 'minutes'),
188 datasets: [{
189 label: 'Response Time (ms)',
190 data: new Array(60).fill(0),
191 borderColor: '#3b82f6',
192 backgroundColor: 'rgba(59, 130, 246, 0.1)',
193 tension: 0.4
194 }]
195 },
196 options: chartOptions
197 });
198 }
199
200 generateTimeLabels(count, unit = 'hours') {
201 const labels = [];
202 for (let i = count - 1; i >= 0; i--) {
203 if (unit === 'hours') {
204 labels.push(`${i}h`);
205 } else {
206 labels.push(`${i}m`);
207 }
208 }
209 return labels;
210 }
211
212 updateCharts() {
213 // Update volume chart with message counts
214 const volumeData = this.calculateVolumeData();
215 this.charts.volume.data.datasets[0].data = volumeData;
216 this.charts.volume.update();
217
218 // Update detection chart
219 const detectionData = this.calculateDetectionData();
220 this.charts.detection.data.datasets[0].data = detectionData;
221 this.charts.detection.update();
222
223 // Update platform distribution
224 const platformData = this.calculatePlatformData();
225 this.charts.platform.data.datasets[0].data = platformData;
226 this.charts.platform.update();
227
228 // Update response times
229 const responseData = this.calculateResponseData();
230 this.charts.response.data.datasets[0].data = responseData;
231 this.charts.response.update();
232 }
233
234 calculateVolumeData() {
235 // Group messages by hour
236 const hourCounts = new Array(24).fill(0);
237 const now = Date.now();
238
239 this.data.messages.forEach(msg => {
240 const age = (now - msg.timestamp) / (1000 * 60 * 60);
241 if (age < 24) {
242 const hour = Math.floor(age);
243 hourCounts[23 - hour]++;
244 }
245 });
246
247 return hourCounts;
248 }
249
250 calculateDetectionData() {
251 const counts = [0, 0, 0, 0]; // Low, Medium, High, Critical
252
253 this.data.scammers.forEach(scammer => {
254 if (scammer.score < 0.3) counts[0]++;
255 else if (scammer.score < 0.6) counts[1]++;
256 else if (scammer.score < 0.9) counts[2]++;
257 else counts[3]++;
258 });
259
260 return counts;
261 }
262
263 calculatePlatformData() {
264 const counts = { whatsapp: 0, telegram: 0, messenger: 0 };
265
266 this.data.conversations.forEach(conv => {
267 counts[conv.platform] = (counts[conv.platform] || 0) + 1;
268 });
269
270 return [counts.whatsapp, counts.telegram, counts.messenger];
271 }
272
273 calculateResponseData() {
274 // Return last 60 response time measurements
275 return this.data.messages
276 .slice(-60)
277 .map(msg => msg.responseTime || 0);
278 }
279
280 addActivityItem(data) {
281 const feed = document.getElementById('activityFeed');
282 const item = document.createElement('div');
283 item.className = 'activity-item';
284
285 if (data.scammerScore > 0.7) {
286 item.classList.add('scammer');
287 }
288
289 const time = new Date(data.timestamp).toLocaleTimeString();
290
291 item.innerHTML = `
292 <span class="activity-time">${time}</span>
293 <span class="activity-message">${data.message || 'New activity'}</span>
294 `;
295
296 feed.insertBefore(item, feed.firstChild);
297
298 // Keep only last 50 items
299 while (feed.children.length > 50) {
300 feed.removeChild(feed.lastChild);
301 }
302 }
303
304 updateMessageStats(data) {
305 this.data.messages.push(data);
306
307 // Update counters
308 document.getElementById('totalMessages').textContent = this.data.messages.length;
309
310 // Calculate and show trend
311 const trend = this.calculateTrend('messages');
312 const trendElement = document.getElementById('messageTrend');
313 trendElement.textContent = `${trend > 0 ? '+' : ''}${trend}%`;
314 trendElement.className = trend >= 0 ? 'stat-trend' : 'stat-trend negative';
315 }
316
317 handleScammerDetection(data) {
318 this.data.scammers.push(data);
319
320 // Update counter
321 document.getElementById('scammersDetected').textContent = this.data.scammers.length;
322
323 // Add special notification
324 this.showNotification(`Scammer detected! Confidence: ${(data.score * 100).toFixed(0)}%`, 'warning');
325
326 // Flash the stat card
327 const card = document.querySelector('.stat-card.danger');
328 card.style.animation = 'pulse 0.5s';
329 setTimeout(() => {
330 card.style.animation = '';
331 }, 500);
332
333 // Add to activity feed
334 this.addActivityItem({
335 ...data,
336 message: `Scammer detected on ${data.platform} (${(data.score * 100).toFixed(0)}% confidence)`
337 });
338 }
339
340 updateActiveSessions(data) {
341 document.getElementById('activeSessions').textContent = data.count || 0;
342 }
343
344 addScreenshot(data) {
345 const grid = document.getElementById('evidenceGrid');
346
347 // Remove placeholder if exists
348 const placeholder = grid.querySelector('.evidence-placeholder');
349 if (placeholder) {
350 placeholder.remove();
351 }
352
353 const item = document.createElement('div');
354 item.className = 'evidence-item';
355 item.innerHTML = `
356 <img src="${data.thumbnail}" alt="Evidence">
357 <div class="evidence-meta">
358 ${data.platform} - ${new Date(data.timestamp).toLocaleString()}
359 </div>
360 `;
361
362 grid.insertBefore(item, grid.firstChild);
363
364 // Keep only last 12 screenshots
365 while (grid.children.length > 12) {
366 grid.removeChild(grid.lastChild);
367 }
368
369 this.data.screenshots.push(data);
370 }
371
372 addPattern(data) {
373 this.data.patterns.push(data);
374
375 // Update pattern stats
376 document.getElementById('totalPatterns').textContent = this.data.patterns.length;
377 document.getElementById('patternAccuracy').textContent =
378 `${(this.calculatePatternAccuracy() * 100).toFixed(0)}%`;
379
380 // Update patterns list
381 const list = document.getElementById('patternsList');
382 const item = document.createElement('div');
383 item.className = 'pattern-item';
384 item.innerHTML = `
385 <span class="pattern-type">${data.type}</span>
386 <span class="pattern-count">${data.occurrences} occurrences</span>
387 `;
388
389 list.insertBefore(item, list.firstChild);
390
391 // Keep only last 10 patterns
392 while (list.children.length > 10) {
393 list.removeChild(list.lastChild);
394 }
395 }
396
397 calculatePatternAccuracy() {
398 if (this.data.patterns.length === 0) return 0;
399
400 const correct = this.data.patterns.filter(p => p.verified).length;
401 return correct / this.data.patterns.length;
402 }
403
404 calculateTrend(metric) {
405 // Simple trend calculation (current hour vs previous hour)
406 const now = Date.now();
407 const hourAgo = now - (60 * 60 * 1000);
408
409 let current = 0;
410 let previous = 0;
411
412 if (metric === 'messages') {
413 this.data.messages.forEach(msg => {
414 if (msg.timestamp > hourAgo) current++;
415 else if (msg.timestamp > (hourAgo - 60 * 60 * 1000)) previous++;
416 });
417 }
418
419 if (previous === 0) return current > 0 ? 100 : 0;
420 return Math.round(((current - previous) / previous) * 100);
421 }
422
423 setupEventListeners() {
424 // Export button
425 document.getElementById('exportBtn').addEventListener('click', () => {
426 this.exportData();
427 });
428
429 // Emergency stop
430 document.getElementById('emergencyStop').addEventListener('click', () => {
431 this.emergencyStop();
432 });
433
434 // Clear data
435 document.getElementById('clearDataBtn').addEventListener('click', () => {
436 this.clearOldData();
437 });
438
439 // Sync patterns
440 document.getElementById('syncBtn').addEventListener('click', () => {
441 this.syncPatterns();
442 });
443
444 // Personality builder
445 document.getElementById('personalityBtn').addEventListener('click', () => {
446 this.openPersonalityBuilder();
447 });
448
449 // Conversation replay
450 document.getElementById('replayBtn').addEventListener('click', () => {
451 this.openConversationReplay();
452 });
453
454 // Modal controls
455 document.getElementById('modalClose').addEventListener('click', () => {
456 document.getElementById('personalityModal').classList.remove('active');
457 });
458
459 document.getElementById('replayClose').addEventListener('click', () => {
460 document.getElementById('replayModal').classList.remove('active');
461 });
462
463 // Personality form
464 document.getElementById('personalityForm').addEventListener('submit', (e) => {
465 e.preventDefault();
466 this.savePersonality();
467 });
468
469 // Test personality
470 document.getElementById('testPersonality').addEventListener('click', () => {
471 this.testPersonality();
472 });
473
474 // Temperature slider
475 document.getElementById('temperature').addEventListener('input', (e) => {
476 document.getElementById('tempValue').textContent = e.target.value;
477 });
478 }
479
480 async exportData() {
481 try {
482 const data = {
483 timestamp: new Date().toISOString(),
484 messages: this.data.messages,
485 scammers: this.data.scammers,
486 patterns: this.data.patterns,
487 screenshots: this.data.screenshots.map(s => s.id),
488 conversations: Array.from(this.data.conversations.values())
489 };
490
491 const blob = new Blob([JSON.stringify(data, null, 2)], {
492 type: 'application/json'
493 });
494
495 const url = URL.createObjectURL(blob);
496 const a = document.createElement('a');
497 a.href = url;
498 a.download = `loosecannon-export-${Date.now()}.json`;
499 a.click();
500
501 this.showNotification('Data exported successfully', 'success');
502 } catch (error) {
503 console.error('Export error:', error);
504 this.showNotification('Export failed', 'error');
505 }
506 }
507
508 async emergencyStop() {
509 if (!confirm('This will stop ALL active LooseCannon sessions. Continue?')) {
510 return;
511 }
512
513 try {
514 const response = await fetch(`${this.serverUrl}/emergency-stop`, {
515 method: 'POST'
516 });
517
518 if (response.ok) {
519 this.showNotification('Emergency stop activated', 'warning');
520
521 // Clear active sessions display
522 document.getElementById('activeSessions').textContent = '0';
523
524 // Send stop command via WebSocket
525 if (this.ws && this.ws.readyState === WebSocket.OPEN) {
526 this.ws.send(JSON.stringify({ type: 'EMERGENCY_STOP' }));
527 }
528 }
529 } catch (error) {
530 console.error('Emergency stop error:', error);
531 }
532 }
533
534 async clearOldData() {
535 if (!confirm('Clear data older than 24 hours?')) {
536 return;
537 }
538
539 const cutoff = Date.now() - (24 * 60 * 60 * 1000);
540
541 // Filter data
542 this.data.messages = this.data.messages.filter(m => m.timestamp > cutoff);
543 this.data.scammers = this.data.scammers.filter(s => s.timestamp > cutoff);
544 this.data.screenshots = this.data.screenshots.filter(s => s.timestamp > cutoff);
545
546 // Update displays
547 this.updateCharts();
548 this.showNotification('Old data cleared', 'success');
549 }
550
551 async syncPatterns() {
552 try {
553 const response = await fetch(`${this.serverUrl}/patterns/sync`, {
554 method: 'GET'
555 });
556
557 if (response.ok) {
558 const patterns = await response.json();
559
560 // Merge with local patterns
561 patterns.forEach(pattern => {
562 if (!this.data.patterns.find(p => p.id === pattern.id)) {
563 this.addPattern(pattern);
564 }
565 });
566
567 this.showNotification(`Synced ${patterns.length} patterns`, 'success');
568 }
569 } catch (error) {
570 console.error('Pattern sync error:', error);
571 this.showNotification('Pattern sync failed', 'error');
572 }
573 }
574
575 openPersonalityBuilder() {
576 document.getElementById('personalityModal').classList.add('active');
577 }
578
579 async savePersonality() {
580 const personality = {
581 id: document.getElementById('personalityName').value.toLowerCase().replace(/\s+/g, '-'),
582 name: document.getElementById('personalityName').value,
583 systemPrompt: document.getElementById('systemPrompt').value,
584 temperature: parseFloat(document.getElementById('temperature').value),
585 examples: document.getElementById('examples').value.split('\n').filter(e => e.trim())
586 };
587
588 try {
589 const response = await fetch(`${this.serverUrl}/personalities`, {
590 method: 'POST',
591 headers: { 'Content-Type': 'application/json' },
592 body: JSON.stringify(personality)
593 });
594
595 if (response.ok) {
596 this.showNotification('Personality saved successfully', 'success');
597 document.getElementById('personalityModal').classList.remove('active');
598 document.getElementById('personalityForm').reset();
599 }
600 } catch (error) {
601 console.error('Save personality error:', error);
602 this.showNotification('Failed to save personality', 'error');
603 }
604 }
605
606 async testPersonality() {
607 const testPrompt = "Hello, I have an important message about your account.";
608 const systemPrompt = document.getElementById('systemPrompt').value;
609 const temperature = parseFloat(document.getElementById('temperature').value);
610
611 try {
612 const response = await fetch(`${this.serverUrl}/test-personality`, {
613 method: 'POST',
614 headers: { 'Content-Type': 'application/json' },
615 body: JSON.stringify({
616 systemPrompt,
617 temperature,
618 message: testPrompt
619 })
620 });
621
622 if (response.ok) {
623 const result = await response.json();
624 document.getElementById('testOutput').innerHTML = `
625 <strong>Test Input:</strong> "${testPrompt}"<br>
626 <strong>Response:</strong> "${result.response}"
627 `;
628 }
629 } catch (error) {
630 console.error('Test personality error:', error);
631 document.getElementById('testOutput').innerHTML = 'Test failed';
632 }
633 }
634
635 openConversationReplay() {
636 document.getElementById('replayModal').classList.add('active');
637 this.loadConversations();
638 }
639
640 async loadConversations() {
641 const select = document.getElementById('conversationSelect');
642 select.innerHTML = '<option>Select a conversation...</option>';
643
644 this.data.conversations.forEach((conv, id) => {
645 const option = document.createElement('option');
646 option.value = id;
647 option.textContent = `${conv.platform} - ${conv.chatId} (${conv.messages.length} messages)`;
648 select.appendChild(option);
649 });
650 }
651
652 startRealtimeUpdates() {
653 // Update uptime
654 setInterval(() => {
655 const uptime = Date.now() - this.startTime;
656 const hours = Math.floor(uptime / (1000 * 60 * 60));
657 const minutes = Math.floor((uptime % (1000 * 60 * 60)) / (1000 * 60));
658 document.getElementById('uptime').textContent = `${hours}h ${minutes}m`;
659 }, 60000);
660
661 // Update last updated
662 setInterval(() => {
663 document.getElementById('lastUpdated').textContent = new Date().toLocaleTimeString();
664 }, 1000);
665
666 // Update response time
667 setInterval(() => {
668 const avgResponse = this.calculateAverageResponseTime();
669 document.getElementById('responseTime').textContent = `${avgResponse}ms`;
670 }, 5000);
671 }
672
673 calculateAverageResponseTime() {
674 if (this.data.messages.length === 0) return 0;
675
676 const recent = this.data.messages.slice(-20);
677 const sum = recent.reduce((acc, msg) => acc + (msg.responseTime || 0), 0);
678 return Math.round(sum / recent.length);
679 }
680
681 async loadInitialData() {
682 try {
683 // Load analytics
684 const analyticsResponse = await fetch(`${this.serverUrl}/analytics`);
685 if (analyticsResponse.ok) {
686 const analytics = await analyticsResponse.json();
687 document.getElementById('totalMessages').textContent = analytics.totalMessages || 0;
688 document.getElementById('scammersDetected').textContent = analytics.scammersDetected || 0;
689 }
690
691 // Load active conversations
692 const conversationsResponse = await fetch(`${this.serverUrl}/conversations/active`);
693 if (conversationsResponse.ok) {
694 const conversations = await conversationsResponse.json();
695 this.updateConversationCards(conversations);
696 }
697
698 // Load patterns
699 const patternsResponse = await fetch(`${this.serverUrl}/patterns`);
700 if (patternsResponse.ok) {
701 const patterns = await patternsResponse.json();
702 patterns.forEach(pattern => this.addPattern(pattern));
703 }
704 } catch (error) {
705 console.error('Error loading initial data:', error);
706 }
707 }
708
709 updateConversationCards(conversations) {
710 const grid = document.getElementById('conversationsGrid');
711 grid.innerHTML = '';
712
713 if (conversations.length === 0) {
714 grid.innerHTML = `
715 <div class="conversation-card">
716 <div class="conversation-header">
717 <span class="conversation-id">No active conversations</span>
718 </div>
719 </div>
720 `;
721 return;
722 }
723
724 conversations.forEach(conv => {
725 this.data.conversations.set(conv.id, conv);
726
727 const card = document.createElement('div');
728 card.className = 'conversation-card';
729 card.innerHTML = `
730 <div class="conversation-header">
731 <span class="platform-badge ${conv.platform}">${conv.platform}</span>
732 <span class="conversation-id">${conv.chatId}</span>
733 </div>
734 <div class="conversation-stats">
735 <span>Messages: ${conv.messageCount}</span>
736 <span>Score: ${conv.scammerScore.toFixed(2)}</span>
737 <span>Duration: ${Math.round(conv.duration / 60000)}m</span>
738 </div>
739 `;
740
741 card.addEventListener('click', () => {
742 this.viewConversation(conv.id);
743 });
744
745 grid.appendChild(card);
746 });
747
748 document.getElementById('activeSessions').textContent = conversations.length;
749 }
750
751 viewConversation(conversationId) {
752 const conv = this.data.conversations.get(conversationId);
753 if (!conv) return;
754
755 // Open replay modal with this conversation
756 document.getElementById('replayModal').classList.add('active');
757 document.getElementById('conversationSelect').value = conversationId;
758 this.loadConversationMessages(conversationId);
759 }
760
761 loadConversationMessages(conversationId) {
762 const conv = this.data.conversations.get(conversationId);
763 if (!conv) return;
764
765 const container = document.getElementById('replayMessages');
766 container.innerHTML = '';
767
768 conv.messages.forEach(msg => {
769 const msgDiv = document.createElement('div');
770 msgDiv.className = `replay-message ${msg.sender}`;
771 msgDiv.innerHTML = `
772 <div class="message-time">${new Date(msg.timestamp).toLocaleTimeString()}</div>
773 <div class="message-content">${msg.content}</div>
774 `;
775 container.appendChild(msgDiv);
776 });
777
778 // Update stats
779 document.getElementById('replayStats').innerHTML = `
780 <span>Messages: ${conv.messages.length}</span>
781 <span>Duration: ${Math.round(conv.duration / 60000)}m</span>
782 <span>Scammer Score: ${conv.scammerScore.toFixed(2)}</span>
783 `;
784 }
785
786 showNotification(message, type = 'info') {
787 // Create notification element
788 const notification = document.createElement('div');
789 notification.className = `notification ${type}`;
790 notification.textContent = message;
791 notification.style.cssText = `
792 position: fixed;
793 top: 80px;
794 right: 20px;
795 padding: 1rem 1.5rem;
796 border-radius: 8px;
797 z-index: 2000;
798 animation: slideIn 0.3s ease;
799 `;
800
801 // Style based on type
802 const colors = {
803 info: '#3b82f6',
804 success: '#10b981',
805 warning: '#f59e0b',
806 error: '#ef4444'
807 };
808
809 notification.style.background = colors[type] || colors.info;
810 notification.style.color = 'white';
811
812 document.body.appendChild(notification);
813
814 // Auto remove after 5 seconds
815 setTimeout(() => {
816 notification.style.animation = 'slideOut 0.3s ease';
817 setTimeout(() => notification.remove(), 300);
818 }, 5000);
819 }
820 }
821
822 // Initialize dashboard when DOM is ready
823 document.addEventListener('DOMContentLoaded', () => {
824 window.dashboard = new LooseCannonDashboard();
825 });