// LooseCannon Analytics Dashboard JavaScript
// Real-time monitoring and control interface
class LooseCannonDashboard {
constructor() {
this.serverUrl = 'http://localhost:8765';
this.ws = null;
this.charts = {};
this.updateInterval = null;
this.startTime = Date.now();
this.data = {
messages: [],
scammers: [],
conversations: new Map(),
patterns: [],
screenshots: []
};
this.init();
}
async init() {
await this.checkServerConnection();
this.initializeCharts();
this.setupEventListeners();
this.connectWebSocket();
this.startRealtimeUpdates();
this.loadInitialData();
}
async checkServerConnection() {
const statusElement = document.getElementById('serverStatus');
try {
const response = await fetch(`${this.serverUrl}/status`);
if (response.ok) {
statusElement.classList.add('connected');
console.log('Server connected');
} else {
throw new Error('Server not responding');
}
} catch (error) {
console.error('Server connection failed:', error);
statusElement.classList.remove('connected');
this.showNotification('Server offline - some features unavailable', 'error');
}
}
connectWebSocket() {
// WebSocket for real-time updates
this.ws = new WebSocket('ws://localhost:8766');
this.ws.onopen = () => {
console.log('WebSocket connected');
this.showNotification('Connected to real-time feed', 'success');
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.handleRealtimeUpdate(data);
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
this.ws.onclose = () => {
console.log('WebSocket disconnected, reconnecting...');
setTimeout(() => this.connectWebSocket(), 5000);
};
}
handleRealtimeUpdate(data) {
switch (data.type) {
case 'MESSAGE':
this.addActivityItem(data);
this.updateMessageStats(data);
break;
case 'SCAMMER_DETECTED':
this.handleScammerDetection(data);
break;
case 'SESSION_UPDATE':
this.updateActiveSessions(data);
break;
case 'SCREENSHOT':
this.addScreenshot(data);
break;
case 'PATTERN_LEARNED':
this.addPattern(data);
break;
}
this.updateCharts();
}
initializeCharts() {
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: '#e0e0e0'
}
}
},
scales: {
y: {
grid: {
color: '#333'
},
ticks: {
color: '#888'
}
},
x: {
grid: {
color: '#333'
},
ticks: {
color: '#888'
}
}
}
};
// Volume Chart
this.charts.volume = new Chart(document.getElementById('volumeChart'), {
type: 'line',
data: {
labels: this.generateTimeLabels(24),
datasets: [{
label: 'Messages',
data: new Array(24).fill(0),
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
tension: 0.4
}]
},
options: chartOptions
});
// Detection Chart
this.charts.detection = new Chart(document.getElementById('detectionChart'), {
type: 'bar',
data: {
labels: ['Low', 'Medium', 'High', 'Critical'],
datasets: [{
label: 'Detections',
data: [0, 0, 0, 0],
backgroundColor: ['#10b981', '#f59e0b', '#ef4444', '#7c3aed']
}]
},
options: chartOptions
});
// Platform Chart
this.charts.platform = new Chart(document.getElementById('platformChart'), {
type: 'doughnut',
data: {
labels: ['WhatsApp', 'Telegram', 'Messenger'],
datasets: [{
data: [0, 0, 0],
backgroundColor: ['#25d366', '#0088cc', '#0084ff']
}]
},
options: {
...chartOptions,
plugins: {
legend: {
position: 'bottom',
labels: {
color: '#e0e0e0'
}
}
}
}
});
// Response Time Chart
this.charts.response = new Chart(document.getElementById('responseChart'), {
type: 'line',
data: {
labels: this.generateTimeLabels(60, 'minutes'),
datasets: [{
label: 'Response Time (ms)',
data: new Array(60).fill(0),
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4
}]
},
options: chartOptions
});
}
generateTimeLabels(count, unit = 'hours') {
const labels = [];
for (let i = count - 1; i >= 0; i--) {
if (unit === 'hours') {
labels.push(`${i}h`);
} else {
labels.push(`${i}m`);
}
}
return labels;
}
updateCharts() {
// Update volume chart with message counts
const volumeData = this.calculateVolumeData();
this.charts.volume.data.datasets[0].data = volumeData;
this.charts.volume.update();
// Update detection chart
const detectionData = this.calculateDetectionData();
this.charts.detection.data.datasets[0].data = detectionData;
this.charts.detection.update();
// Update platform distribution
const platformData = this.calculatePlatformData();
this.charts.platform.data.datasets[0].data = platformData;
this.charts.platform.update();
// Update response times
const responseData = this.calculateResponseData();
this.charts.response.data.datasets[0].data = responseData;
this.charts.response.update();
}
calculateVolumeData() {
// Group messages by hour
const hourCounts = new Array(24).fill(0);
const now = Date.now();
this.data.messages.forEach(msg => {
const age = (now - msg.timestamp) / (1000 * 60 * 60);
if (age < 24) {
const hour = Math.floor(age);
hourCounts[23 - hour]++;
}
});
return hourCounts;
}
calculateDetectionData() {
const counts = [0, 0, 0, 0]; // Low, Medium, High, Critical
this.data.scammers.forEach(scammer => {
if (scammer.score < 0.3) counts[0]++;
else if (scammer.score < 0.6) counts[1]++;
else if (scammer.score < 0.9) counts[2]++;
else counts[3]++;
});
return counts;
}
calculatePlatformData() {
const counts = { whatsapp: 0, telegram: 0, messenger: 0 };
this.data.conversations.forEach(conv => {
counts[conv.platform] = (counts[conv.platform] || 0) + 1;
});
return [counts.whatsapp, counts.telegram, counts.messenger];
}
calculateResponseData() {
// Return last 60 response time measurements
return this.data.messages
.slice(-60)
.map(msg => msg.responseTime || 0);
}
addActivityItem(data) {
const feed = document.getElementById('activityFeed');
const item = document.createElement('div');
item.className = 'activity-item';
if (data.scammerScore > 0.7) {
item.classList.add('scammer');
}
const time = new Date(data.timestamp).toLocaleTimeString();
item.innerHTML = `
${time}
${data.message || 'New activity'}
`;
feed.insertBefore(item, feed.firstChild);
// Keep only last 50 items
while (feed.children.length > 50) {
feed.removeChild(feed.lastChild);
}
}
updateMessageStats(data) {
this.data.messages.push(data);
// Update counters
document.getElementById('totalMessages').textContent = this.data.messages.length;
// Calculate and show trend
const trend = this.calculateTrend('messages');
const trendElement = document.getElementById('messageTrend');
trendElement.textContent = `${trend > 0 ? '+' : ''}${trend}%`;
trendElement.className = trend >= 0 ? 'stat-trend' : 'stat-trend negative';
}
handleScammerDetection(data) {
this.data.scammers.push(data);
// Update counter
document.getElementById('scammersDetected').textContent = this.data.scammers.length;
// Add special notification
this.showNotification(`Scammer detected! Confidence: ${(data.score * 100).toFixed(0)}%`, 'warning');
// Flash the stat card
const card = document.querySelector('.stat-card.danger');
card.style.animation = 'pulse 0.5s';
setTimeout(() => {
card.style.animation = '';
}, 500);
// Add to activity feed
this.addActivityItem({
...data,
message: `Scammer detected on ${data.platform} (${(data.score * 100).toFixed(0)}% confidence)`
});
}
updateActiveSessions(data) {
document.getElementById('activeSessions').textContent = data.count || 0;
}
addScreenshot(data) {
const grid = document.getElementById('evidenceGrid');
// Remove placeholder if exists
const placeholder = grid.querySelector('.evidence-placeholder');
if (placeholder) {
placeholder.remove();
}
const item = document.createElement('div');
item.className = 'evidence-item';
item.innerHTML = `
${data.platform} - ${new Date(data.timestamp).toLocaleString()}
`;
grid.insertBefore(item, grid.firstChild);
// Keep only last 12 screenshots
while (grid.children.length > 12) {
grid.removeChild(grid.lastChild);
}
this.data.screenshots.push(data);
}
addPattern(data) {
this.data.patterns.push(data);
// Update pattern stats
document.getElementById('totalPatterns').textContent = this.data.patterns.length;
document.getElementById('patternAccuracy').textContent =
`${(this.calculatePatternAccuracy() * 100).toFixed(0)}%`;
// Update patterns list
const list = document.getElementById('patternsList');
const item = document.createElement('div');
item.className = 'pattern-item';
item.innerHTML = `
${data.type}
${data.occurrences} occurrences
`;
list.insertBefore(item, list.firstChild);
// Keep only last 10 patterns
while (list.children.length > 10) {
list.removeChild(list.lastChild);
}
}
calculatePatternAccuracy() {
if (this.data.patterns.length === 0) return 0;
const correct = this.data.patterns.filter(p => p.verified).length;
return correct / this.data.patterns.length;
}
calculateTrend(metric) {
// Simple trend calculation (current hour vs previous hour)
const now = Date.now();
const hourAgo = now - (60 * 60 * 1000);
let current = 0;
let previous = 0;
if (metric === 'messages') {
this.data.messages.forEach(msg => {
if (msg.timestamp > hourAgo) current++;
else if (msg.timestamp > (hourAgo - 60 * 60 * 1000)) previous++;
});
}
if (previous === 0) return current > 0 ? 100 : 0;
return Math.round(((current - previous) / previous) * 100);
}
setupEventListeners() {
// Export button
document.getElementById('exportBtn').addEventListener('click', () => {
this.exportData();
});
// Emergency stop
document.getElementById('emergencyStop').addEventListener('click', () => {
this.emergencyStop();
});
// Clear data
document.getElementById('clearDataBtn').addEventListener('click', () => {
this.clearOldData();
});
// Sync patterns
document.getElementById('syncBtn').addEventListener('click', () => {
this.syncPatterns();
});
// Personality builder
document.getElementById('personalityBtn').addEventListener('click', () => {
this.openPersonalityBuilder();
});
// Conversation replay
document.getElementById('replayBtn').addEventListener('click', () => {
this.openConversationReplay();
});
// Modal controls
document.getElementById('modalClose').addEventListener('click', () => {
document.getElementById('personalityModal').classList.remove('active');
});
document.getElementById('replayClose').addEventListener('click', () => {
document.getElementById('replayModal').classList.remove('active');
});
// Personality form
document.getElementById('personalityForm').addEventListener('submit', (e) => {
e.preventDefault();
this.savePersonality();
});
// Test personality
document.getElementById('testPersonality').addEventListener('click', () => {
this.testPersonality();
});
// Temperature slider
document.getElementById('temperature').addEventListener('input', (e) => {
document.getElementById('tempValue').textContent = e.target.value;
});
}
async exportData() {
try {
const data = {
timestamp: new Date().toISOString(),
messages: this.data.messages,
scammers: this.data.scammers,
patterns: this.data.patterns,
screenshots: this.data.screenshots.map(s => s.id),
conversations: Array.from(this.data.conversations.values())
};
const blob = new Blob([JSON.stringify(data, null, 2)], {
type: 'application/json'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `loosecannon-export-${Date.now()}.json`;
a.click();
this.showNotification('Data exported successfully', 'success');
} catch (error) {
console.error('Export error:', error);
this.showNotification('Export failed', 'error');
}
}
async emergencyStop() {
if (!confirm('This will stop ALL active LooseCannon sessions. Continue?')) {
return;
}
try {
const response = await fetch(`${this.serverUrl}/emergency-stop`, {
method: 'POST'
});
if (response.ok) {
this.showNotification('Emergency stop activated', 'warning');
// Clear active sessions display
document.getElementById('activeSessions').textContent = '0';
// Send stop command via WebSocket
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'EMERGENCY_STOP' }));
}
}
} catch (error) {
console.error('Emergency stop error:', error);
}
}
async clearOldData() {
if (!confirm('Clear data older than 24 hours?')) {
return;
}
const cutoff = Date.now() - (24 * 60 * 60 * 1000);
// Filter data
this.data.messages = this.data.messages.filter(m => m.timestamp > cutoff);
this.data.scammers = this.data.scammers.filter(s => s.timestamp > cutoff);
this.data.screenshots = this.data.screenshots.filter(s => s.timestamp > cutoff);
// Update displays
this.updateCharts();
this.showNotification('Old data cleared', 'success');
}
async syncPatterns() {
try {
const response = await fetch(`${this.serverUrl}/patterns/sync`, {
method: 'GET'
});
if (response.ok) {
const patterns = await response.json();
// Merge with local patterns
patterns.forEach(pattern => {
if (!this.data.patterns.find(p => p.id === pattern.id)) {
this.addPattern(pattern);
}
});
this.showNotification(`Synced ${patterns.length} patterns`, 'success');
}
} catch (error) {
console.error('Pattern sync error:', error);
this.showNotification('Pattern sync failed', 'error');
}
}
openPersonalityBuilder() {
document.getElementById('personalityModal').classList.add('active');
}
async savePersonality() {
const personality = {
id: document.getElementById('personalityName').value.toLowerCase().replace(/\s+/g, '-'),
name: document.getElementById('personalityName').value,
systemPrompt: document.getElementById('systemPrompt').value,
temperature: parseFloat(document.getElementById('temperature').value),
examples: document.getElementById('examples').value.split('\n').filter(e => e.trim())
};
try {
const response = await fetch(`${this.serverUrl}/personalities`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(personality)
});
if (response.ok) {
this.showNotification('Personality saved successfully', 'success');
document.getElementById('personalityModal').classList.remove('active');
document.getElementById('personalityForm').reset();
}
} catch (error) {
console.error('Save personality error:', error);
this.showNotification('Failed to save personality', 'error');
}
}
async testPersonality() {
const testPrompt = "Hello, I have an important message about your account.";
const systemPrompt = document.getElementById('systemPrompt').value;
const temperature = parseFloat(document.getElementById('temperature').value);
try {
const response = await fetch(`${this.serverUrl}/test-personality`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
systemPrompt,
temperature,
message: testPrompt
})
});
if (response.ok) {
const result = await response.json();
document.getElementById('testOutput').innerHTML = `
Test Input: "${testPrompt}"
Response: "${result.response}"
`;
}
} catch (error) {
console.error('Test personality error:', error);
document.getElementById('testOutput').innerHTML = 'Test failed';
}
}
openConversationReplay() {
document.getElementById('replayModal').classList.add('active');
this.loadConversations();
}
async loadConversations() {
const select = document.getElementById('conversationSelect');
select.innerHTML = '';
this.data.conversations.forEach((conv, id) => {
const option = document.createElement('option');
option.value = id;
option.textContent = `${conv.platform} - ${conv.chatId} (${conv.messages.length} messages)`;
select.appendChild(option);
});
}
startRealtimeUpdates() {
// Update uptime
setInterval(() => {
const uptime = Date.now() - this.startTime;
const hours = Math.floor(uptime / (1000 * 60 * 60));
const minutes = Math.floor((uptime % (1000 * 60 * 60)) / (1000 * 60));
document.getElementById('uptime').textContent = `${hours}h ${minutes}m`;
}, 60000);
// Update last updated
setInterval(() => {
document.getElementById('lastUpdated').textContent = new Date().toLocaleTimeString();
}, 1000);
// Update response time
setInterval(() => {
const avgResponse = this.calculateAverageResponseTime();
document.getElementById('responseTime').textContent = `${avgResponse}ms`;
}, 5000);
}
calculateAverageResponseTime() {
if (this.data.messages.length === 0) return 0;
const recent = this.data.messages.slice(-20);
const sum = recent.reduce((acc, msg) => acc + (msg.responseTime || 0), 0);
return Math.round(sum / recent.length);
}
async loadInitialData() {
try {
// Load analytics
const analyticsResponse = await fetch(`${this.serverUrl}/analytics`);
if (analyticsResponse.ok) {
const analytics = await analyticsResponse.json();
document.getElementById('totalMessages').textContent = analytics.totalMessages || 0;
document.getElementById('scammersDetected').textContent = analytics.scammersDetected || 0;
}
// Load active conversations
const conversationsResponse = await fetch(`${this.serverUrl}/conversations/active`);
if (conversationsResponse.ok) {
const conversations = await conversationsResponse.json();
this.updateConversationCards(conversations);
}
// Load patterns
const patternsResponse = await fetch(`${this.serverUrl}/patterns`);
if (patternsResponse.ok) {
const patterns = await patternsResponse.json();
patterns.forEach(pattern => this.addPattern(pattern));
}
} catch (error) {
console.error('Error loading initial data:', error);
}
}
updateConversationCards(conversations) {
const grid = document.getElementById('conversationsGrid');
grid.innerHTML = '';
if (conversations.length === 0) {
grid.innerHTML = `
`;
return;
}
conversations.forEach(conv => {
this.data.conversations.set(conv.id, conv);
const card = document.createElement('div');
card.className = 'conversation-card';
card.innerHTML = `
Messages: ${conv.messageCount}
Score: ${conv.scammerScore.toFixed(2)}
Duration: ${Math.round(conv.duration / 60000)}m
`;
card.addEventListener('click', () => {
this.viewConversation(conv.id);
});
grid.appendChild(card);
});
document.getElementById('activeSessions').textContent = conversations.length;
}
viewConversation(conversationId) {
const conv = this.data.conversations.get(conversationId);
if (!conv) return;
// Open replay modal with this conversation
document.getElementById('replayModal').classList.add('active');
document.getElementById('conversationSelect').value = conversationId;
this.loadConversationMessages(conversationId);
}
loadConversationMessages(conversationId) {
const conv = this.data.conversations.get(conversationId);
if (!conv) return;
const container = document.getElementById('replayMessages');
container.innerHTML = '';
conv.messages.forEach(msg => {
const msgDiv = document.createElement('div');
msgDiv.className = `replay-message ${msg.sender}`;
msgDiv.innerHTML = `
${new Date(msg.timestamp).toLocaleTimeString()}
${msg.content}
`;
container.appendChild(msgDiv);
});
// Update stats
document.getElementById('replayStats').innerHTML = `
Messages: ${conv.messages.length}
Duration: ${Math.round(conv.duration / 60000)}m
Scammer Score: ${conv.scammerScore.toFixed(2)}
`;
}
showNotification(message, type = 'info') {
// Create notification element
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 80px;
right: 20px;
padding: 1rem 1.5rem;
border-radius: 8px;
z-index: 2000;
animation: slideIn 0.3s ease;
`;
// Style based on type
const colors = {
info: '#3b82f6',
success: '#10b981',
warning: '#f59e0b',
error: '#ef4444'
};
notification.style.background = colors[type] || colors.info;
notification.style.color = 'white';
document.body.appendChild(notification);
// Auto remove after 5 seconds
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease';
setTimeout(() => notification.remove(), 300);
}, 5000);
}
}
// Initialize dashboard when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
window.dashboard = new LooseCannonDashboard();
});