Files
honeypot.es/static/dashboard.js
Malin 1aa164263f feat: initial T-Pot attack map with Spanish UI and Docker support
- Full Spanish interface (all UI text, popups, charts, tables)
- Dark and light mode support
- Disclaimer banner: no data logged, public European service
- Footer: Servicio ofrecido por Cloud Host (cloudhost.es)
- Docker: single container (Redis + DataServer + AttackMapServer)
- Remote T-Pot support via ELASTICSEARCH_URL env var (direct or SSH tunnel)
- Based on telekom-security/t-pot-attack-map (Apache 2.0)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 21:23:28 +01:00

3680 lines
136 KiB
JavaScript

/**
* T-Pot Attack Map Dashboard
* Enhanced UI and data visualization functionality
*/
/**
* Attack Cache Manager
* Handles persistent storage of attack events using IndexedDB with LocalStorage fallback
*/
class AttackCache {
constructor() {
this.dbName = 'TPotAttackCache';
this.storeName = 'attacks';
this.version = 1;
this.retentionPeriod = 24 * 60 * 60 * 1000; // 24 hours
this.maxEvents = 10000; // Performance/memory limit
this.cleanupInterval = 5 * 60 * 1000; // Cleanup every 5 minutes
this.storageType = null;
this.db = null;
this.localStorageKey = 'tpot_attack_cache';
}
async init() {
console.log('[CACHE] Initializing attack cache...');
try {
await this.initIndexedDB();
this.storageType = 'indexeddb';
console.log('[CACHE] Using IndexedDB for storage');
} catch (error) {
console.warn('[CACHE] IndexedDB failed, falling back to LocalStorage:', error);
this.initLocalStorage();
this.storageType = 'localstorage';
console.log('[CACHE] Using LocalStorage for storage');
}
// Start periodic cleanup
setInterval(() => {
this.cleanup();
}, this.cleanupInterval);
console.log(`[CACHE] Cache initialized with ${this.storageType}, retention: 24h`);
}
async initIndexedDB() {
return new Promise((resolve, reject) => {
if (!window.indexedDB) {
reject(new Error('IndexedDB not supported'));
return;
}
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create attacks object store if it doesn't exist
if (!db.objectStoreNames.contains(this.storeName)) {
const store = db.createObjectStore(this.storeName, {
keyPath: 'id',
autoIncrement: true
});
// Create index on timestamp for efficient cleanup queries
store.createIndex('timestamp', 'timestamp', { unique: false });
store.createIndex('source_ip', 'source_ip', { unique: false });
store.createIndex('honeypot', 'honeypot', { unique: false });
}
};
});
}
initLocalStorage() {
// LocalStorage is synchronous, just verify it's available
if (!window.localStorage) {
throw new Error('LocalStorage not supported');
}
// Initialize empty cache if doesn't exist
if (!localStorage.getItem(this.localStorageKey)) {
const emptyCache = {
events: [],
lastCleanup: Date.now(),
version: 1
};
localStorage.setItem(this.localStorageKey, JSON.stringify(emptyCache));
}
}
async storeEvent(event) {
// Add timestamp and unique ID if not present
const eventToStore = {
...event,
timestamp: event.timestamp || Date.now(),
cached_at: Date.now()
};
try {
if (this.storageType === 'indexeddb') {
await this.storeEventIndexedDB(eventToStore);
} else {
this.storeEventLocalStorage(eventToStore);
}
} catch (error) {
console.warn('[CACHE] Failed to store event:', error);
}
}
async storeEventIndexedDB(event) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.add(event);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
storeEventLocalStorage(event) {
const cache = JSON.parse(localStorage.getItem(this.localStorageKey) || '{"events":[]}');
cache.events.push(event);
// Keep within limits
if (cache.events.length > this.maxEvents) {
cache.events = cache.events.slice(-this.maxEvents);
}
localStorage.setItem(this.localStorageKey, JSON.stringify(cache));
}
async getStoredEvents() {
try {
if (this.storageType === 'indexeddb') {
return await this.getStoredEventsIndexedDB();
} else {
return this.getStoredEventsLocalStorage();
}
} catch (error) {
console.warn('[CACHE] Failed to retrieve stored events:', error);
return [];
}
}
async getStoredEventsIndexedDB() {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const index = store.index('timestamp');
// Get events from last 24 hours
const cutoff = Date.now() - this.retentionPeriod;
const range = IDBKeyRange.lowerBound(cutoff);
const request = index.getAll(range);
request.onsuccess = () => {
const events = request.result || [];
console.log(`[CACHE] Retrieved ${events.length} events from IndexedDB`);
resolve(events);
};
request.onerror = () => reject(request.error);
});
}
getStoredEventsLocalStorage() {
const cache = JSON.parse(localStorage.getItem(this.localStorageKey) || '{"events":[]}');
const cutoff = Date.now() - this.retentionPeriod;
// Filter events within retention period
const validEvents = cache.events.filter(event =>
event.timestamp && event.timestamp > cutoff
);
console.log(`[CACHE] Retrieved ${validEvents.length} events from LocalStorage`);
return validEvents;
}
async cleanup() {
try {
if (this.storageType === 'indexeddb') {
await this.cleanupIndexedDB();
} else {
this.cleanupLocalStorage();
}
} catch (error) {
console.warn('[CACHE] Cleanup failed:', error);
}
}
async cleanupIndexedDB() {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const index = store.index('timestamp');
// Delete events older than retention period
const cutoff = Date.now() - this.retentionPeriod;
const range = IDBKeyRange.upperBound(cutoff);
const request = index.openCursor(range);
let deletedCount = 0;
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
cursor.delete();
deletedCount++;
cursor.continue();
} else {
if (deletedCount > 0) {
console.log(`[CACHE] Cleaned up ${deletedCount} old events from IndexedDB`);
}
resolve();
}
};
request.onerror = () => reject(request.error);
});
}
cleanupLocalStorage() {
const cache = JSON.parse(localStorage.getItem(this.localStorageKey) || '{"events":[]}');
const cutoff = Date.now() - this.retentionPeriod;
const originalCount = cache.events.length;
// Keep only events within retention period
cache.events = cache.events.filter(event =>
event.timestamp && event.timestamp > cutoff
);
// Limit total events for performance
if (cache.events.length > this.maxEvents) {
cache.events = cache.events.slice(-this.maxEvents);
}
cache.lastCleanup = Date.now();
localStorage.setItem(this.localStorageKey, JSON.stringify(cache));
const deletedCount = originalCount - cache.events.length;
if (deletedCount > 0) {
console.log(`[CACHE] Cleaned up ${deletedCount} old events from LocalStorage`);
}
}
async getStatistics() {
const events = await this.getStoredEvents();
return {
totalEvents: events.length,
storageType: this.storageType,
oldestEvent: events.length > 0 ? Math.min(...events.map(e => e.timestamp)) : null,
newestEvent: events.length > 0 ? Math.max(...events.map(e => e.timestamp)) : null,
retentionPeriod: this.retentionPeriod
};
}
async clearCache() {
console.log('[CACHE] Clearing all cached data...');
try {
if (this.storageType === 'indexeddb') {
await this.clearCacheIndexedDB();
} else {
this.clearCacheLocalStorage();
}
console.log('[CACHE] Cache cleared successfully');
} catch (error) {
console.error('[CACHE] Failed to clear cache:', error);
throw error;
}
}
async clearCacheIndexedDB() {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.clear();
request.onsuccess = () => {
console.log('[CACHE] IndexedDB cache cleared');
resolve();
};
request.onerror = () => reject(request.error);
});
}
clearCacheLocalStorage() {
const emptyCache = {
events: [],
lastCleanup: Date.now(),
version: 1
};
localStorage.setItem(this.localStorageKey, JSON.stringify(emptyCache));
console.log('[CACHE] LocalStorage cache cleared');
}
}
class AttackMapDashboard {
constructor() {
this.charts = {};
this.theme = localStorage.getItem('theme') || 'dark';
this.panelCollapsed = localStorage.getItem('sidePanelCollapsed') === 'true' || false;
this.bottomPanelHeight = parseInt(localStorage.getItem('bottomPanelHeight')) || 350;
this.activeTab = 'live-feed';
this.searchFilters = {};
this.settings = this.loadSettings();
this.attackHistory = [];
this.protocolStats = {};
this.countryStats = {};
this.timelineInterval = '24h'; // Default to 24 hours
this.timelineData = this.initializeTimelineData(); // Add timeline data structure
this.lastTimelineUpdate = Date.now(); // Track last update time
this.connectionStatus = 'connecting'; // Track current connection status
// Honeypot Performance Tracking
this.honeypotStats = {
data: new Map(), // Map to store honeypot stats
retention: 15 * 60 * 1000, // 15 minutes in milliseconds
updateInterval: 60000, // Update chart every minute
lastCleanup: Date.now()
};
// Initialize attack cache system
this.attackCache = new AttackCache();
this.cacheInitialized = false;
this.restoringFromCache = false;
this.init();
}
async init() {
this.initTheme();
this.initEventListeners();
this.initCharts();
this.initThreatHeatmap();
this.initPanels();
this.initTabs();
this.initSearch();
this.initSettings();
this.initSoundSystem();
this.initHoneypotTracking();
// Initialize cache system
await this.initializeCache();
this.hideLoadingScreen();
// Start background tasks
this.startPerformanceMonitoring();
this.startDataAggregation();
this.startConnectionStatusMonitoring();
}
async initializeCache() {
try {
await this.attackCache.init();
this.cacheInitialized = true;
// Update cache status UI
this.updateCacheStatus();
// Try to restore data from cache
await this.restoreFromCache();
console.log('[DASHBOARD] Cache system initialized successfully');
} catch (error) {
console.error('[DASHBOARD] Failed to initialize cache:', error);
this.cacheInitialized = false;
this.updateCacheStatus('error');
}
}
updateCacheStatus(status = null) {
const cacheStatus = document.getElementById('cache-status');
const cacheIndicator = document.getElementById('cache-indicator');
const cacheText = document.getElementById('cache-text');
if (!cacheStatus || !cacheIndicator || !cacheText) return;
if (!this.cacheInitialized || status === 'error') {
cacheStatus.style.display = 'none';
return;
}
// Show cache status
cacheStatus.style.display = 'flex';
// Set status based on cache state
if (status === 'restoring') {
cacheIndicator.className = 'status-indicator connecting';
cacheText.textContent = 'Restoring...';
cacheStatus.title = 'Restoring data from cache';
} else {
cacheIndicator.className = 'status-indicator cached';
cacheText.textContent = 'Cached';
// Update tooltip with cache info
this.attackCache.getStatistics().then(stats => {
const storageType = stats.storageType === 'indexeddb' ? 'IndexedDB' : 'LocalStorage';
cacheStatus.title = `${stats.totalEvents} events cached (${storageType})`;
}).catch(() => {
cacheStatus.title = 'Data cache active';
});
}
// Add click handler to show cache details
cacheStatus.onclick = () => this.showCacheDetails();
}
async showCacheDetails() {
try {
const stats = await this.attackCache.getStatistics();
const storageType = stats.storageType === 'indexeddb' ? 'IndexedDB' : 'LocalStorage';
const oldestDate = stats.oldestEvent ? new Date(stats.oldestEvent).toLocaleString() : 'None';
const newestDate = stats.newestEvent ? new Date(stats.newestEvent).toLocaleString() : 'None';
const message = `
Cache Statistics:
• Storage: ${storageType}
• Events: ${stats.totalEvents}
• Oldest: ${oldestDate}
• Newest: ${newestDate}
• Retention: 24 hours
`;
this.showNotification(message.trim(), 'info', 'cache');
} catch (error) {
console.error('[CACHE] Failed to get statistics:', error);
this.showNotification('Failed to get cache statistics', 'error', 'cache');
}
}
async restoreFromCache() {
console.log('[DASHBOARD] Attempting to restore data from cache...');
this.restoringFromCache = true;
this.updateCacheStatus('restoring');
try {
const cachedEvents = await this.attackCache.getStoredEvents();
const stats = await this.attackCache.getStatistics();
console.log(`[DASHBOARD] Found ${cachedEvents.length} cached events (${stats.storageType})`);
if (cachedEvents.length > 0) {
// Sort events by timestamp (oldest first)
cachedEvents.sort((a, b) => a.timestamp - b.timestamp);
// Restore all cached events for complete statistics
this.attackHistory = cachedEvents.map(event => ({
...event,
restored: true // Mark as restored for debugging
}));
// Process events in chunks to prevent UI blocking
const BATCH_SIZE = 500;
let processedCount = 0;
const processBatch = () => {
const end = Math.min(processedCount + BATCH_SIZE, cachedEvents.length);
// Process a batch of events
for (let i = processedCount; i < end; i++) {
const event = cachedEvents[i];
// Process for timeline
this.addAttackToTimeline(event, false); // false = don't trigger updates
// Process for heatmap
this.addAttackToHeatmap(event, false);
// Process for honeypot tracking
if (event.honeypot) {
this.trackHoneypotAttack(event.honeypot, event.timestamp, false);
}
// Update tracking data (needed for top IPs and top countries)
this.updateIPTracking(event.ip || event.source_ip, event.country, event);
this.updateCountryTracking(event.country, event);
this.updateProtocolStats(event.protocol);
}
processedCount = end;
if (processedCount < cachedEvents.length) {
// Schedule next batch
requestAnimationFrame(processBatch);
} else {
// All batches complete - finalize restoration
this.finalizeRestoration(cachedEvents, stats);
}
};
// Start processing batches
processBatch();
} else {
console.log('[DASHBOARD] No cached events found');
this.restoringFromCache = false;
this.updateCacheStatus();
}
} catch (error) {
console.error('[DASHBOARD] Failed to restore from cache:', error);
this.showNotification('Failed to restore cached data', 'error', 'cache');
this.restoringFromCache = false;
this.updateCacheStatus();
}
}
finalizeRestoration(cachedEvents, stats) {
try {
// Restore map markers
// We need to find the last 200 UNIQUE locations to match the map's visual limit.
const mapEvents = [];
const uniqueLocations = new Set();
const MAX_MAP_CIRCLES = 200;
// Iterate backwards to find the most recent unique locations
for (let i = cachedEvents.length - 1; i >= 0; i--) {
const event = cachedEvents[i];
// Use coordinates as key if available, otherwise fallback to IP
const key = (event.source_lat && event.source_lng)
? `${event.source_lat},${event.source_lng}`
: (event.source_ip || event.ip);
if (key) {
if (uniqueLocations.size < MAX_MAP_CIRCLES || uniqueLocations.has(key)) {
mapEvents.push(event);
uniqueLocations.add(key);
}
}
}
// Reverse to restore chronological order (oldest to newest)
mapEvents.reverse();
console.log(`[DASHBOARD] Restoring ${mapEvents.length} events covering ${uniqueLocations.size} unique locations`);
for (const event of mapEvents) {
if (typeof window.processRestoredAttack === 'function') {
window.processRestoredAttack(event);
}
}
// Restore live feed (last 100 events, newest first)
const liveFeedEvents = cachedEvents.slice(-100);
for (const event of liveFeedEvents) {
this.addToAttackTable(event, false);
}
// Update all visualizations with restored data
this.aggregateProtocolStats();
this.aggregateCountryStats();
this.updateTimelineChart();
this.updateHoneypotChartData();
this.updateThreatHeatmap();
this.updateDashboardMetrics();
// Update tables with aggregated data
this.updateTopIPsTable();
this.updateTopCountriesTable();
console.log(`[DASHBOARD] Successfully restored ${cachedEvents.length} events from cache`);
// Show restoration notification
this.showNotification(
`${cachedEvents.length} eventos restaurados desde caché (${this.formatTimeAgo(stats.oldestEvent)})`,
'success',
'cache'
);
} catch (error) {
console.error('[DASHBOARD] Error during final restoration steps:', error);
} finally {
this.restoringFromCache = false;
this.updateCacheStatus();
}
}
formatTimeAgo(timestamp) {
if (!timestamp) return 'unknown';
const now = Date.now();
const diff = now - timestamp;
const minutes = Math.floor(diff / (1000 * 60));
const hours = Math.floor(diff / (1000 * 60 * 60));
if (hours > 0) {
return `${hours}h ago`;
} else if (minutes > 0) {
return `${minutes}m ago`;
} else {
return 'just now';
}
}
getProtocolColor(protocol, port = null) {
const colors = {
'CHARGEN': '#4CAF50',
'FTP-DATA': '#F44336',
'FTP': '#FF5722',
'SSH': '#FF9800',
'TELNET': '#FFC107',
'SMTP': '#8BC34A',
'WINS': '#009688',
'DNS': '#00BCD4',
'DHCP': '#03A9F4',
'TFTP': '#2196F3',
'HTTP': '#3F51B5',
'DICOM': '#9C27B0',
'POP3': '#E91E63',
'NTP': '#795548',
'RPC': '#607D8B',
'IMAP': '#9E9E9E',
'SNMP': '#FF6B35',
'LDAP': '#FF8E53',
'HTTPS': '#0080FF',
'SMB': '#BF00FF',
'SMTPS': '#80FF00',
'EMAIL': '#00FF80',
'IPMI': '#00FFFF',
'IPP': '#8000FF',
'IMAPS': '#FF0080',
'POP3S': '#80FF80',
'NFS': '#FF8080',
'SOCKS': '#8080FF',
'SQL': '#00FF00',
'ORACLE': '#FFFF00',
'PPTP': '#FF00FF',
'MQTT': '#00FF40',
'SSDP': '#40FF00',
'IEC104': '#FF4000',
'HL7': '#4000FF',
'MYSQL': '#00FF00',
'RDP': '#FF0060',
'IPSEC': '#60FF00',
'SIP': '#FFCCFF',
'POSTGRESQL': '#00CCFF',
'ADB': '#FFCCCC',
'VNC': '#0000FF',
'REDIS': '#CC00FF',
'IRC': '#FFCC00',
'JETDIRECT': '#8000FF',
'ELASTICSEARCH': '#FF8000',
'INDUSTRIAL': '#80FF40',
'MEMCACHED': '#40FF80',
'MONGODB': '#FF4080',
'SCADA': '#8040FF',
'OTHER': '#78909C'
};
const protocolUpper = protocol?.toUpperCase();
// Return predefined color for known protocols
if (colors[protocolUpper]) {
return colors[protocolUpper];
}
// Fallback for unknown protocols - should use OTHER color for consistency
return colors['OTHER']; // Use OTHER color (#78909C) for unknown protocols
}
// Normalize protocol names to known protocols or "OTHER"
normalizeProtocol(protocol) {
if (!protocol) return 'OTHER';
// Check if protocol is a numeric string (port number) - convert to OTHER
if (/^\d+$/.test(protocol.toString())) {
return 'OTHER';
}
// List of known protocols to check against
const knownProtocols = [
'CHARGEN', 'FTP-DATA', 'FTP', 'SSH', 'TELNET', 'SMTP', 'WINS', 'DNS', 'DHCP', 'TFTP',
'HTTP', 'DICOM', 'POP3', 'NTP', 'RPC', 'IMAP', 'SNMP', 'LDAP', 'HTTPS', 'SMB',
'SMTPS', 'EMAIL', 'IPMI', 'IPP', 'IMAPS', 'POP3S', 'NFS', 'SOCKS', 'SQL', 'ORACLE',
'PPTP', 'MQTT', 'SSDP', 'IEC104', 'HL7', 'MYSQL', 'RDP', 'IPSEC', 'SIP', 'POSTGRESQL',
'ADB', 'VNC', 'REDIS', 'IRC', 'JETDIRECT', 'ELASTICSEARCH', 'INDUSTRIAL', 'MEMCACHED',
'MONGODB', 'SCADA'
];
const protocolUpper = protocol.toUpperCase();
// If protocol is not in the known list, use "OTHER"
if (!knownProtocols.includes(protocolUpper)) {
return 'OTHER';
}
return protocolUpper;
}
// Initialize timeline data structure for different intervals
initializeTimelineData() {
return this.generateTimelineData(this.timelineInterval);
}
// Generate timeline data based on interval
generateTimelineData(interval) {
const timeline = [];
const now = new Date();
let points, duration, unit;
switch (interval) {
case '1m':
points = 60; // 60 seconds
duration = 1000; // 1 second
unit = 'second';
break;
case '1h':
points = 60; // 60 minutes
duration = 60 * 1000; // 1 minute
unit = 'minute';
break;
case '24h':
default:
points = 24; // 24 hours
duration = 60 * 60 * 1000; // 1 hour
unit = 'hour';
break;
}
// Create data points for the specified interval
for (let i = points - 1; i >= 0; i--) {
const time = new Date(now.getTime() - i * duration);
let label, timestamp;
switch (interval) {
case '1m':
label = time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
timestamp = Math.floor(time.getTime() / 1000) * 1000; // Round to second
break;
case '1h':
label = time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
timestamp = Math.floor(time.getTime() / (60 * 1000)) * (60 * 1000); // Round to minute
break;
case '24h':
default:
label = time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
timestamp = Math.floor(time.getTime() / (60 * 60 * 1000)) * (60 * 60 * 1000); // Round to hour
break;
}
timeline.push({
timestamp: timestamp,
label: label,
count: 0, // Initialize with zero, will be populated with real data
unit: unit
});
}
return timeline;
}
// Populate timeline with real attack data from history
populateTimelineFromHistory() {
if (!this.attackHistory.length) {
console.log(`[DEBUG] No attack history available for timeline population`);
return;
}
console.log(`[DEBUG] Populating timeline (${this.timelineInterval}) from ${this.attackHistory.length} attacks`);
// Reset counts
this.timelineData.forEach(point => {
point.count = 0;
});
let attacksInRange = 0;
// Count attacks in each time bucket
this.attackHistory.forEach(attack => {
const attackTime = new Date(attack.timestamp || attack.time || attack.date || Date.now());
// Find the appropriate timeline bucket
this.timelineData.forEach(point => {
const pointTime = new Date(point.timestamp);
const duration = this.getDurationForInterval();
const pointEndTime = new Date(pointTime.getTime() + duration);
// Check if attack falls within this time bucket
if (attackTime >= pointTime && attackTime < pointEndTime) {
point.count++;
attacksInRange++;
}
});
});
console.log(`[DEBUG] Timeline populated: ${attacksInRange} attacks in range, counts:`, this.timelineData.map(p => p.count));
this.updateTimelineChart();
}
// Get duration in milliseconds for current interval
getDurationForInterval() {
switch (this.timelineInterval) {
case '1m': return 1000; // 1 second
case '1h': return 60 * 1000; // 1 minute
case '24h':
default: return 60 * 60 * 1000; // 1 hour
}
}
// Add new attack to timeline data
addAttackToTimeline(attack, triggerUpdate = true) {
const attackTime = new Date(attack.timestamp || attack.time || attack.date || Date.now());
const duration = this.getDurationForInterval();
let attackAdded = false;
// Try to find existing time bucket that contains this attack
for (let i = 0; i < this.timelineData.length; i++) {
const point = this.timelineData[i];
const pointTime = new Date(point.timestamp);
const pointEndTime = new Date(pointTime.getTime() + duration);
// Check if attack falls within this time bucket
if (attackTime >= pointTime && attackTime < pointEndTime) {
point.count++;
attackAdded = true;
console.log(`[DEBUG] Added attack to existing bucket ${i}: ${point.label}, new count: ${point.count}`);
break;
}
}
// If attack doesn't fit in any existing bucket, check if we need to add new buckets
if (!attackAdded) {
const lastPoint = this.timelineData[this.timelineData.length - 1];
const lastPointTime = new Date(lastPoint.timestamp);
const lastPointEndTime = new Date(lastPointTime.getTime() + duration);
// If attack is after the last bucket, add new bucket(s)
if (attackTime >= lastPointEndTime) {
let currentTime = lastPointEndTime;
// Add buckets until we reach the attack time
while (currentTime <= attackTime) {
const bucketStartTime = new Date(Math.floor(currentTime.getTime() / duration) * duration);
const isAttackBucket = attackTime >= bucketStartTime && attackTime < new Date(bucketStartTime.getTime() + duration);
const newPoint = {
timestamp: bucketStartTime.getTime(),
label: this.formatTimeLabel(bucketStartTime),
count: isAttackBucket ? 1 : 0,
unit: this.getUnitForInterval()
};
// Remove oldest point and add new one
this.timelineData.shift();
this.timelineData.push(newPoint);
if (isAttackBucket) {
attackAdded = true;
console.log(`[DEBUG] Added attack to new bucket: ${newPoint.label}, count: 1`);
break;
}
currentTime = new Date(currentTime.getTime() + duration);
}
} else {
console.log(`[DEBUG] Attack is too old to fit in current timeline window`);
}
}
if (attackAdded && triggerUpdate) {
this.updateTimelineChart();
}
}
// Helper to format time labels
formatTimeLabel(time) {
switch (this.timelineInterval) {
case '1m':
return time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
case '1h':
case '24h':
default:
return time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
}
// Helper to get unit for interval
getUnitForInterval() {
switch (this.timelineInterval) {
case '1m': return 'second';
case '1h': return 'minute';
case '24h':
default: return 'hour';
}
}
// Update timeline chart with current data
updateTimelineChart() {
if (!this.charts.timeline) return;
const labels = this.timelineData.map(point => point.label);
const data = this.timelineData.map(point => point.count);
this.charts.timeline.data.labels = labels;
this.charts.timeline.data.datasets[0].data = data;
this.charts.timeline.update('none'); // Use 'none' mode for better performance
}
// Convert HSL to RGB
hslToRgb(h, s, l) {
let r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return [r * 255, g * 255, b * 255];
}
// Convert hex color to RGB
hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
// Theme Management
initTheme() {
document.documentElement.setAttribute('data-theme', this.theme);
this.updateThemeIcon();
}
toggleTheme() {
this.theme = this.theme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', this.theme);
localStorage.setItem('theme', this.theme);
this.updateThemeIcon();
this.updateChartsTheme();
// Update map theme if the function exists
if (typeof updateMapTheme === 'function') {
updateMapTheme(this.theme);
}
}
updateThemeIcon() {
const icon = document.querySelector('#theme-toggle i');
if (icon) {
icon.className = this.theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
}
}
// Event Listeners
initEventListeners() {
// Theme toggle
document.getElementById('theme-toggle')?.addEventListener('click', () => {
this.toggleTheme();
});
// Fullscreen toggle
document.getElementById('fullscreen-toggle')?.addEventListener('click', () => {
this.toggleFullscreen();
});
// Settings modal
document.getElementById('settings-toggle')?.addEventListener('click', () => {
this.openSettings();
});
document.getElementById('settings-close')?.addEventListener('click', () => {
this.closeSettings();
});
// Panel controls
document.getElementById('panel-toggle')?.addEventListener('click', () => {
this.toggleSidePanel();
});
// Panel resizing
this.initPanelResize();
// Settings
document.getElementById('save-settings')?.addEventListener('click', () => {
this.saveSettings();
});
document.getElementById('reset-settings')?.addEventListener('click', () => {
this.resetSettings();
});
document.getElementById('clear-cache')?.addEventListener('click', () => {
this.clearCache();
});
// Timeline interval selector
document.getElementById('timeline-interval')?.addEventListener('change', (e) => {
this.changeTimelineInterval(e.target.value);
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
this.handleKeyboardShortcuts(e);
});
// Window resize
window.addEventListener('resize', () => {
this.handleWindowResize();
});
}
// Panel Management
initPanels() {
this.updatePanelStates();
this.initPanelResize();
this.updateSidePanelHeight(); // Initial side panel height setup
// Apply saved side panel state
this.applySidePanelState();
}
toggleSidePanel() {
this.panelCollapsed = !this.panelCollapsed;
// Save the panel state to localStorage
localStorage.setItem('sidePanelCollapsed', this.panelCollapsed.toString());
const panel = document.getElementById('side-panel');
if (panel) {
panel.classList.toggle('collapsed', this.panelCollapsed);
}
this.updateMapSize();
}
applySidePanelState() {
const panel = document.getElementById('side-panel');
if (panel && this.panelCollapsed) {
panel.classList.add('collapsed');
}
}
initPanelResize() {
const resizeHandle = document.getElementById('panel-resize');
const bottomPanel = document.getElementById('bottom-panel');
if (!resizeHandle || !bottomPanel) return;
let isResizing = false;
let startY = 0;
let startHeight = 0;
resizeHandle.addEventListener('mousedown', (e) => {
isResizing = true;
startY = e.clientY;
startHeight = this.bottomPanelHeight;
document.body.style.cursor = 'ns-resize';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const deltaY = startY - e.clientY;
const newHeight = Math.max(200, Math.min(600, startHeight + deltaY));
this.bottomPanelHeight = newHeight;
localStorage.setItem('bottomPanelHeight', newHeight.toString());
bottomPanel.style.height = `${newHeight}px`;
this.updateMapSize(); // This will also update side panel height
});
document.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
document.body.style.cursor = '';
}
});
}
updatePanelStates() {
const bottomPanel = document.getElementById('bottom-panel');
if (bottomPanel) {
bottomPanel.style.height = `${this.bottomPanelHeight}px`;
}
this.updateMapSize();
}
updateMapSize() {
// Update side panel height based on bottom panel position
this.updateSidePanelHeight();
// Trigger map resize if needed
if (window.map) {
setTimeout(() => {
window.map.invalidateSize();
}, 300);
}
}
updateSidePanelHeight() {
const sidePanel = document.getElementById('side-panel');
const bottomPanel = document.getElementById('bottom-panel');
if (sidePanel && bottomPanel) {
// Calculate available height: viewport height - navbar height - bottom panel height
const navbarHeight = 70; // Top navbar height
const bottomPanelHeight = this.bottomPanelHeight || 350;
const availableHeight = window.innerHeight - navbarHeight - bottomPanelHeight;
sidePanel.style.height = `${Math.max(200, availableHeight)}px`;
}
}
// Tab Management
initTabs() {
const tabButtons = document.querySelectorAll('.tab-btn');
tabButtons.forEach(btn => {
btn.addEventListener('click', () => {
const tabName = btn.getAttribute('data-tab');
this.switchTab(tabName);
});
});
// Activate the default tab (live-feed)
this.switchTab(this.activeTab);
}
switchTab(tabName) {
// Update active tab
this.activeTab = tabName;
// Update tab buttons
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('data-tab') === tabName);
});
// Hide all tab panes first
document.querySelectorAll('.tab-pane').forEach(pane => {
pane.classList.remove('active');
pane.style.display = 'none';
});
// Show the selected tab pane
const targetTab = document.getElementById(`${tabName}-tab`);
if (targetTab) {
targetTab.classList.add('active');
targetTab.style.display = 'block';
}
// Load tab-specific data
this.loadTabData(tabName);
// Resize charts if needed
setTimeout(() => {
this.resizeCharts();
}, 100);
}
loadTabData(tabName) {
switch (tabName) {
case 'overview':
this.updateOverviewCharts();
break;
case 'top-ips':
this.updateTopIPsTable();
break;
case 'countries':
this.updateTopCountriesTable();
break;
case 'live-feed':
this.updateLiveFeed();
break;
}
}
// Chart Management
initCharts() {
// Initialize charts with a slight delay to ensure containers are properly sized
setTimeout(() => {
this.initAttackDistributionChart();
this.initTimelineChart();
this.initProtocolChart();
this.initHoneypotChart();
// Populate timeline and heatmap with any existing attack history
setTimeout(() => {
this.populateTimelineFromHistory();
this.populateHeatmapFromHistory();
}, 200);
// Force a resize after initialization
setTimeout(() => {
this.resizeCharts();
}, 100);
}, 50);
}
// Handle timeline interval change
changeTimelineInterval(newInterval) {
console.log(`[DEBUG] Changing timeline interval from ${this.timelineInterval} to ${newInterval}`);
this.timelineInterval = newInterval;
this.timelineData = this.generateTimelineData(newInterval);
// Populate the new timeline with existing attack history
this.populateTimelineFromHistory();
// Reinitialize the timeline chart with new data
this.initTimelineChart();
// Restart timeline updates with new frequency
this.startTimelineUpdates();
console.log(`[DEBUG] Timeline interval changed to ${newInterval}, data points: ${this.timelineData.length}`);
}
// Update timeline chart title based on interval
updateTimelineChartTitle() {
const titleMap = {
'1m': 'Ataques por Segundo (Último Minuto)',
'1h': 'Ataques por Minuto (Última Hora)',
'24h': 'Ataques por Hora (Últimas 24 Horas)'
};
if (this.charts.timeline && this.charts.timeline.options.plugins.legend) {
this.charts.timeline.data.datasets[0].label = titleMap[this.timelineInterval] || 'Ataques';
this.charts.timeline.options.scales.x.title.text = this.getTimelineXAxisTitle();
this.charts.timeline.options.scales.y.title.text = 'Cantidad de Ataques';
this.charts.timeline.update();
}
}
// Get X-axis title based on interval
getTimelineXAxisTitle() {
const titleMap = {
'1m': 'Hora (Último Minuto)',
'1h': 'Hora (Última Hora)',
'24h': 'Hora (Últimas 24 Horas)'
};
return titleMap[this.timelineInterval] || 'Hora';
}
initAttackDistributionChart() {
const ctx = document.getElementById('attack-distribution-chart');
if (!ctx) return;
const labels = ['SSH', 'HTTP', 'FTP', 'TELNET', 'OTHER'];
const colors = labels.map(protocol => {
if (protocol === 'OTHER') {
return this.getProtocolColor(protocol, 8080); // Default port for initial display
}
return this.getProtocolColor(protocol);
});
this.charts.attackDistribution = new Chart(ctx, {
type: 'doughnut',
data: {
labels: labels,
datasets: [{
data: [30, 25, 15, 10, 20],
backgroundColor: colors,
borderWidth: 2,
borderColor: colors
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
layout: {
padding: {
top: 10,
bottom: 10,
left: 10,
right: 10
}
},
plugins: {
legend: {
position: 'bottom',
labels: {
color: '#b0b0b0',
usePointStyle: true,
padding: 10,
boxWidth: 12,
font: {
size: 11
}
}
}
},
elements: {
arc: {
borderWidth: 2
}
},
cutout: '60%'
}
});
// Ensure proper sizing after chart creation
setTimeout(() => {
if (this.charts.attackDistribution) {
this.charts.attackDistribution.resize();
}
}, 100);
}
initTimelineChart() {
const ctx = document.getElementById('timeline-chart');
if (!ctx) return;
// Destroy existing chart if it exists
if (this.charts.timeline) {
this.charts.timeline.destroy();
}
// Use the structured timeline data
const labels = this.timelineData.map(point => point.label);
const data = this.timelineData.map(point => point.count);
// Get dynamic titles based on current interval
const datasetLabel = this.getDatasetLabel();
const xAxisTitle = this.getTimelineXAxisTitle();
this.charts.timeline = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: datasetLabel,
data: data,
borderColor: '#e20074',
backgroundColor: 'rgba(226, 0, 116, 0.1)',
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
ticks: {
color: '#b0b0b0',
maxTicksLimit: this.getMaxTicks()
},
grid: { color: '#333' },
title: {
display: false
}
},
y: {
ticks: { color: '#b0b0b0' },
grid: { color: '#333' },
title: {
display: false
}
}
},
plugins: {
legend: {
position: 'bottom',
align: 'center',
labels: {
color: '#b0b0b0',
usePointStyle: true,
padding: 15,
font: {
size: 11
}
}
},
tooltip: {
callbacks: {
label: function(context) {
return `Ataques: ${context.parsed.y}`;
}
}
}
},
animation: {
duration: this.timelineInterval === '1m' ? 0 : 750
},
transitions: {
active: {
animation: {
duration: this.timelineInterval === '1m' ? 0 : 400
}
}
}
}
});
}
// Get dataset label based on current interval
getDatasetLabel() {
const labelMap = {
'1m': 'Ataques por Segundo',
'1h': 'Ataques por Minuto',
'24h': 'Ataques por Hora'
};
return labelMap[this.timelineInterval] || 'Ataques';
}
// Get max ticks for X-axis based on interval
getMaxTicks() {
const tickMap = {
'1m': 6, // Show every 10 seconds (fewer ticks for longer HH:MM:SS labels)
'1h': 12, // Show every 5 minutes
'24h': 12 // Show every 2 hours
};
return tickMap[this.timelineInterval] || 12;
}
initProtocolChart() {
const ctx = document.getElementById('protocol-chart');
if (!ctx) return;
const labels = ['SSH', 'HTTP', 'FTP', 'TELNET', 'DNS', 'SMTP'];
const colors = labels.map(protocol => this.getProtocolColor(protocol));
const initialData = [45, 35, 20, 15, 10, 8];
this.charts.protocol = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Ataques',
data: initialData.map(d => Math.sqrt(d)),
originalData: initialData,
backgroundColor: colors,
borderRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
ticks: { color: '#b0b0b0' },
grid: { display: false }
},
y: {
ticks: {
color: '#b0b0b0',
callback: function(value) {
return Math.round(Math.pow(value, 2));
}
},
grid: { color: '#333' }
}
},
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function(context) {
const raw = context.dataset.originalData ?
context.dataset.originalData[context.dataIndex] :
Math.round(Math.pow(context.raw, 2));
return context.dataset.label + ': ' + raw;
}
}
}
}
}
});
}
initHoneypotChart() {
const ctx = document.getElementById('honeypot-chart');
if (!ctx) return;
// Get theme colors - same as timeline chart
const textColor = this.theme === 'dark' ? '#b0b0b0' : '#495057';
const gridColor = this.theme === 'dark' ? '#333' : '#dee2e6';
// Use same transparency as timeline chart
const backgroundColor = 'rgba(226, 0, 116, 0.1)'; // Match timeline exactly
this.charts.honeypot = new Chart(ctx, {
type: 'radar',
data: {
labels: ['Sin datos'],
datasets: [{
label: 'Ataques (Últimos 15m)',
data: [0],
borderColor: '#e20074',
backgroundColor: backgroundColor, // Match timeline transparency
pointBackgroundColor: '#e20074',
pointBorderColor: '#e20074',
pointRadius: 4,
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
r: {
beginAtZero: true,
ticks: {
display: false, // Remove attack count numbers for cleaner look
maxTicksLimit: 8 // Limit number of grid rings to prevent performance issues
},
grid: { color: gridColor },
pointLabels: {
color: textColor,
font: {
size: 10, // Slightly smaller to reduce crowding
weight: '400' // Normal weight for better readability
},
padding: 8, // Add padding to prevent overlapping
backdropColor: 'transparent' // Remove background
},
angleLines: {
color: gridColor
}
}
},
plugins: {
legend: {
position: 'bottom',
align: 'center',
labels: {
color: textColor, // Match timeline chart exactly
usePointStyle: true,
padding: 15,
font: {
size: 11
},
generateLabels: function(chart) {
const original = Chart.defaults.plugins.legend.labels.generateLabels;
const labels = original.call(this, chart);
// Customize legend to match timeline chart opacity and stroke
labels.forEach(label => {
if (label.fillStyle) {
// Use the same transparency as timeline chart
label.fillStyle = 'rgba(226, 0, 116, 0.1)';
label.strokeStyle = '#e20074';
label.lineWidth = 1; // Match timeline chart default stroke width
}
});
return labels;
}
}
},
tooltip: {
backgroundColor: this.theme === 'dark' ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.95)',
titleColor: textColor,
bodyColor: textColor,
borderColor: gridColor,
borderWidth: 1,
callbacks: {
label: function(context) {
const raw = context.dataset.originalData ?
context.dataset.originalData[context.dataIndex] :
Math.round(Math.pow(context.raw, 2));
return context.dataset.label + ': ' + raw;
}
}
}
}
}
});
// Don't call updateHoneypotChartData here - let it be called when real data arrives
}
// Honeypot Performance Tracking System
initHoneypotTracking() {
// Start periodic cleanup of old data
setInterval(() => {
this.cleanupOldHoneypotData();
}, 5 * 60 * 1000); // Clean up every 5 minutes
// Honeypot chart updates will be synchronized with timeline updates
// No separate interval needed - updates happen in addAttackEvent and startDataAggregation
console.log('[DEBUG] Honeypot performance tracking initialized');
}
trackHoneypotAttack(honeypot, timestamp = Date.now(), triggerUpdate = true) {
if (!honeypot || typeof honeypot !== 'string') {
console.warn('[DEBUG] Invalid honeypot data:', honeypot);
return;
}
// Clean honeypot name (remove any whitespace/special chars)
honeypot = honeypot.trim();
// Get or create honeypot entry
if (!this.honeypotStats.data.has(honeypot)) {
this.honeypotStats.data.set(honeypot, []);
console.log(`[DEBUG] New honeypot discovered: ${honeypot}`);
}
// Add timestamp to honeypot's attack history
this.honeypotStats.data.get(honeypot).push(timestamp);
// Immediately clean old data for this honeypot
this.cleanupHoneypotData(honeypot);
const currentStats = this.honeypotStats.data.get(honeypot);
if (currentStats) {
console.log(`[DEBUG] Tracked attack for honeypot: ${honeypot} (${currentStats.length} total attacks)`);
} else {
console.log(`[DEBUG] Tracked attack for honeypot: ${honeypot} (expired/cleaned up)`);
}
}
cleanupOldHoneypotData() {
const cutoff = Date.now() - this.honeypotStats.retention;
for (const [honeypot, timestamps] of this.honeypotStats.data.entries()) {
this.cleanupHoneypotData(honeypot, cutoff);
}
this.honeypotStats.lastCleanup = Date.now();
}
cleanupHoneypotData(honeypot, cutoff = null) {
if (!cutoff) {
cutoff = Date.now() - this.honeypotStats.retention;
}
const timestamps = this.honeypotStats.data.get(honeypot);
if (!timestamps) return;
// Filter out old timestamps
const filtered = timestamps.filter(ts => ts > cutoff);
if (filtered.length === 0) {
// Remove honeypot if no recent attacks
this.honeypotStats.data.delete(honeypot);
} else {
this.honeypotStats.data.set(honeypot, filtered);
}
}
getHoneypotStats() {
const stats = {};
for (const [honeypot, timestamps] of this.honeypotStats.data.entries()) {
stats[honeypot] = timestamps.length;
}
return stats;
}
updateHoneypotChartData() {
if (!this.charts.honeypot) return;
const stats = this.getHoneypotStats();
const honeypots = Object.keys(stats).sort();
const counts = honeypots.map(hp => stats[hp]);
// Handle no data case
if (honeypots.length === 0) {
this.charts.honeypot.data.labels = ['Sin datos'];
this.charts.honeypot.data.datasets[0].data = [0];
this.charts.honeypot.data.datasets[0].originalData = [0];
} else {
// Update chart data with real honeypot data
this.charts.honeypot.data.labels = honeypots;
this.charts.honeypot.data.datasets[0].data = counts.map(c => Math.sqrt(c));
this.charts.honeypot.data.datasets[0].originalData = counts;
}
// Update chart
this.charts.honeypot.update('none'); // No animation for performance
}
// Public method to be called from map.js when processing attacks
processAttackForDashboard(attackData) {
if (!attackData) return;
// Track honeypot attack if honeypot field is present
if (attackData.honeypot) {
this.trackHoneypotAttack(attackData.honeypot, attackData.timestamp || Date.now());
}
// Add to attack history for other dashboard features
this.attackHistory.push({
...attackData,
timestamp: attackData.timestamp || Date.now()
});
// Keep attack history within reasonable bounds (match cache size)
if (this.attackHistory.length > this.attackCache.maxEvents) {
this.attackHistory = this.attackHistory.slice(-this.attackCache.maxEvents);
}
}
updateChartsTheme() {
const textColor = this.theme === 'dark' ? '#b0b0b0' : '#495057';
const gridColor = this.theme === 'dark' ? '#333' : '#dee2e6';
Object.values(this.charts).forEach(chart => {
if (chart && chart.options) {
// Update text colors
if (chart.options.scales) {
Object.values(chart.options.scales).forEach(scale => {
if (scale.ticks) {
scale.ticks.color = textColor;
scale.ticks.backdropColor = 'transparent'; // Ensure transparent backdrop
scale.ticks.showLabelBackdrop = false;
}
if (scale.grid) scale.grid.color = gridColor;
if (scale.pointLabels) {
scale.pointLabels.color = textColor;
scale.pointLabels.backdropColor = 'transparent'; // Ensure transparent backdrop
}
if (scale.angleLines) scale.angleLines.color = gridColor;
});
}
// Update legend colors - all charts follow theme now
if (chart.options.plugins && chart.options.plugins.legend) {
chart.options.plugins.legend.labels.color = textColor;
}
// Update tooltip colors for theme
if (chart.options.plugins && chart.options.plugins.tooltip) {
chart.options.plugins.tooltip.backgroundColor = this.theme === 'dark' ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.95)';
chart.options.plugins.tooltip.titleColor = textColor;
chart.options.plugins.tooltip.bodyColor = textColor;
chart.options.plugins.tooltip.borderColor = gridColor;
}
// Update honeypot chart dataset colors to match timeline chart exactly
if (chart === this.charts.honeypot && chart.data.datasets[0]) {
// Use same transparency as timeline chart (0.1) regardless of theme
const backgroundColor = 'rgba(226, 0, 116, 0.1)';
chart.data.datasets[0].backgroundColor = backgroundColor;
}
chart.update();
}
});
}
resizeCharts() {
Object.values(this.charts).forEach(chart => {
if (chart && chart.resize) {
chart.resize();
}
});
}
// Search and Filtering
initSearch() {
const ipSearch = document.getElementById('ip-search');
const countrySearch = document.getElementById('country-search');
if (ipSearch) {
ipSearch.addEventListener('input', (e) => {
this.filterTable('ip', e.target.value);
});
}
if (countrySearch) {
countrySearch.addEventListener('input', (e) => {
this.filterTable('country', e.target.value);
});
}
}
filterTable(type, query) {
const tableId = type === 'ip' ? 'ip-tracking' : 'country-tracking';
const tbody = document.getElementById(tableId);
if (!tbody) return;
const rows = tbody.querySelectorAll('tr');
const searchTerm = query.toLowerCase();
rows.forEach(row => {
const cells = row.querySelectorAll('td');
let visible = false;
cells.forEach(cell => {
if (cell.textContent.toLowerCase().includes(searchTerm)) {
visible = true;
}
});
row.style.display = visible ? '' : 'none';
});
}
// Data Management
updateOverviewCharts() {
// Update charts with current data
this.updateAttackDistribution();
// Timeline is updated by its own interval
this.updateProtocolBreakdown();
// Update honeypot performance (now part of overview)
this.updateHoneypotPerformance();
}
updateTopIPs() {
// Update top IPs display
console.log('[DEBUG] Updating top IPs display');
// This could refresh the IP tracking table
}
updateCountries() {
// Update countries display
console.log('[DEBUG] Updating countries display');
// This could refresh the country tracking table
}
updateLiveFeed() {
// Update live feed display
console.log('[DEBUG] Updating live feed display');
// This could refresh the live attack feed
}
updateHoneypotPerformance() {
// Force update of honeypot performance chart with latest data
this.updateHoneypotChartData();
}
updateAttackDistribution() {
if (this.charts.attackDistribution && this.protocolStats) {
const data = Object.values(this.protocolStats);
const labels = Object.keys(this.protocolStats);
const colors = labels.map(protocol => {
if (protocol?.toUpperCase() === 'OTHER') {
// Get the most common port for OTHER protocol
const port = this.getMostCommonOtherPort();
return this.getProtocolColor(protocol, port);
}
return this.getProtocolColor(protocol);
});
this.charts.attackDistribution.data.labels = labels;
this.charts.attackDistribution.data.datasets[0].data = data;
this.charts.attackDistribution.data.datasets[0].backgroundColor = colors;
this.charts.attackDistribution.data.datasets[0].borderColor = colors;
this.charts.attackDistribution.update();
}
}
// Get the most common port for OTHER protocol attacks
getMostCommonOtherPort() {
const recent = this.attackHistory.filter(attack =>
Date.now() - attack.timestamp < 300000 &&
attack.protocol?.toUpperCase() === 'OTHER'
);
if (recent.length === 0) return null;
// Count port frequencies
const portCounts = recent.reduce((counts, attack) => {
if (attack.dstPort) {
counts[attack.dstPort] = (counts[attack.dstPort] || 0) + 1;
}
return counts;
}, {});
// Return the most frequent port
const sortedPorts = Object.entries(portCounts)
.sort(([,a], [,b]) => b - a);
return sortedPorts.length > 0 ? parseInt(sortedPorts[0][0]) : null;
}
updateTimeline() {
if (!this.charts.timeline) return;
const now = Date.now();
let currentUnit, lastDataPoint, shouldAddNewPoint = false;
// Get the current time unit based on interval
switch (this.timelineInterval) {
case '1m':
currentUnit = Math.floor(now / 1000); // Current second
lastDataPoint = this.timelineData[this.timelineData.length - 1];
const lastSecond = Math.floor(lastDataPoint.timestamp / 1000);
shouldAddNewPoint = currentUnit > lastSecond;
break;
case '1h':
currentUnit = Math.floor(now / (60 * 1000)); // Current minute
lastDataPoint = this.timelineData[this.timelineData.length - 1];
const lastMinute = Math.floor(lastDataPoint.timestamp / (60 * 1000));
shouldAddNewPoint = currentUnit > lastMinute;
break;
case '24h':
default:
currentUnit = Math.floor(now / (60 * 60 * 1000)); // Current hour
lastDataPoint = this.timelineData[this.timelineData.length - 1];
const lastHour = Math.floor(lastDataPoint.timestamp / (60 * 60 * 1000));
shouldAddNewPoint = currentUnit > lastHour;
break;
}
if (shouldAddNewPoint) {
// Add new time unit
const newTime = new Date(now);
let newTimestamp, newLabel;
switch (this.timelineInterval) {
case '1m':
newTimestamp = Math.floor(now / 1000) * 1000;
newLabel = newTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
break;
case '1h':
newTimestamp = Math.floor(now / (60 * 1000)) * (60 * 1000);
newTime.setSeconds(0, 0);
newLabel = newTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
break;
case '24h':
default:
newTimestamp = Math.floor(now / (60 * 60 * 1000)) * (60 * 60 * 1000);
newTime.setMinutes(0, 0, 0);
newLabel = newTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
break;
}
this.timelineData.push({
timestamp: newTimestamp,
label: newLabel,
count: this.getAttackCountForInterval(),
unit: this.timelineData[0]?.unit || 'hour'
});
// Remove oldest data point to maintain window size
const maxPoints = this.timelineInterval === '1m' ? 60 : this.timelineInterval === '1h' ? 60 : 24;
if (this.timelineData.length > maxPoints) {
this.timelineData.shift();
}
// Update chart with new data
const labels = this.timelineData.map(point => point.label);
const data = this.timelineData.map(point => point.count);
this.charts.timeline.data.labels = labels;
this.charts.timeline.data.datasets[0].data = data;
// Disable animation for 1-minute view to improve readability
const updateMode = this.timelineInterval === '1m' ? 'none' : 'default';
this.charts.timeline.update(updateMode);
console.log(`[DEBUG] Timeline updated for ${this.timelineInterval}, new point added`);
} else {
// Update current time unit's count in real-time
const currentCount = this.getAttackCountForCurrentInterval();
if (currentCount !== lastDataPoint.count) {
lastDataPoint.count = currentCount;
// Update chart without animation for real-time feel
this.charts.timeline.data.datasets[0].data[this.timelineData.length - 1] = currentCount;
this.charts.timeline.update('none');
}
}
}
// Get attack count for the last complete interval
getAttackCountForInterval() {
let intervalMs, lookBackMs;
switch (this.timelineInterval) {
case '1m':
intervalMs = 1000; // 1 second
lookBackMs = 2000; // Look back 2 seconds
break;
case '1h':
intervalMs = 60 * 1000; // 1 minute
lookBackMs = 2 * 60 * 1000; // Look back 2 minutes
break;
case '24h':
default:
intervalMs = 60 * 60 * 1000; // 1 hour
lookBackMs = 2 * 60 * 60 * 1000; // Look back 2 hours
break;
}
const now = Date.now();
const intervalStart = now - lookBackMs;
const intervalEnd = now - intervalMs;
return this.attackHistory.filter(attack =>
attack.timestamp > intervalStart && attack.timestamp <= intervalEnd
).length;
}
// Get attack count for the current ongoing interval
getAttackCountForCurrentInterval() {
let intervalStart;
const now = Date.now();
switch (this.timelineInterval) {
case '1m':
intervalStart = Math.floor(now / 1000) * 1000; // Start of current second
break;
case '1h':
intervalStart = Math.floor(now / (60 * 1000)) * (60 * 1000); // Start of current minute
break;
case '24h':
default:
intervalStart = Math.floor(now / (60 * 60 * 1000)) * (60 * 60 * 1000); // Start of current hour
break;
}
return this.attackHistory.filter(attack =>
attack.timestamp >= intervalStart
).length;
}
updateProtocolBreakdown() {
if (this.charts.protocol && this.protocolStats) {
const data = Object.values(this.protocolStats);
const labels = Object.keys(this.protocolStats);
const colors = labels.map(protocol => this.getProtocolColor(protocol));
this.charts.protocol.data.labels = labels;
this.charts.protocol.data.datasets[0].data = data.map(d => Math.sqrt(d));
this.charts.protocol.data.datasets[0].originalData = data;
this.charts.protocol.data.datasets[0].backgroundColor = colors;
this.charts.protocol.update();
}
}
getRecentAttackCount() {
const now = Date.now();
const oneMinuteAgo = now - 60000;
return this.attackHistory.filter(attack =>
attack.timestamp > oneMinuteAgo
).length;
}
// Settings Management
initSettings() {
this.loadSettingsUI();
}
loadSettings() {
const defaultSettings = {
soundAlerts: false,
alertSound: 'beep'
};
const saved = localStorage.getItem('attack-map-settings');
return saved ? { ...defaultSettings, ...JSON.parse(saved) } : defaultSettings;
}
loadSettingsUI() {
Object.keys(this.settings).forEach(key => {
const element = document.getElementById(key.replace(/([A-Z])/g, '-$1').toLowerCase());
if (element) {
if (element.type === 'checkbox') {
element.checked = this.settings[key];
} else {
element.value = this.settings[key];
}
}
});
// Apply settings after loading them into UI
this.applySettings();
}
saveSettings() {
const settings = {};
// Collect all settings from UI
['sound-alerts', 'alert-sound'].forEach(id => {
const element = document.getElementById(id);
if (element) {
const key = id.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
settings[key] = element.type === 'checkbox' ? element.checked : element.value;
}
});
this.settings = settings;
localStorage.setItem('attack-map-settings', JSON.stringify(settings));
this.applySettings();
this.closeSettings();
this.showNotification('Configuración guardada', 'success', 'settings');
}
resetSettings() {
localStorage.removeItem('attack-map-settings');
localStorage.removeItem('sidePanelCollapsed');
localStorage.removeItem('bottomPanelHeight');
this.settings = this.loadSettings();
// Reset panel states to defaults
this.panelCollapsed = false;
this.bottomPanelHeight = 350;
// Apply the reset panel states
const sidePanel = document.getElementById('side-panel');
const bottomPanel = document.getElementById('bottom-panel');
if (sidePanel) {
sidePanel.classList.remove('collapsed');
}
if (bottomPanel) {
bottomPanel.style.height = '350px';
}
this.loadSettingsUI();
this.applySettings();
this.updateMapSize(); // Update layout after reset
// Show notification after layout updates to prevent position jumping
setTimeout(() => {
this.showNotification('Configuración restablecida', 'info', 'settings');
}, 50);
}
applySettings() {
// Apply sound alert toggle visibility - use actual settings value, not just checkbox state
const soundOptions = document.getElementById('sound-options');
const soundAlerts = document.getElementById('sound-alerts');
if (soundOptions && soundAlerts) {
// Use the stored setting value to determine visibility
soundOptions.style.display = this.settings.soundAlerts ? 'block' : 'none';
// Ensure checkbox matches the setting
soundAlerts.checked = this.settings.soundAlerts;
}
}
async clearCache() {
try {
// Show confirmation dialog
const confirmed = confirm('Are you sure you want to clear all cached data? This will reset the map markers and live feed. This action cannot be undone.');
if (!confirmed) {
return;
}
// Clear the attack cache
if (this.attackCache) {
await this.attackCache.clearCache();
}
// Clear map markers and data
if (window.map) {
// Clear Leaflet map layers
if (window.circles) window.circles.clearLayers();
if (window.markers) window.markers.clearLayers();
if (window.attackLines) window.attackLines.clearLayers();
// Clear map data objects
if (window.circleAttackData) {
Object.keys(window.circleAttackData).forEach(key => {
delete window.circleAttackData[key];
});
}
if (window.markerAttackData) {
Object.keys(window.markerAttackData).forEach(key => {
delete window.markerAttackData[key];
});
}
if (window.circlesObject) {
Object.keys(window.circlesObject).forEach(key => {
delete window.circlesObject[key];
});
}
if (window.markersObject) {
Object.keys(window.markersObject).forEach(key => {
delete window.markersObject[key];
});
}
}
// Clear dashboard tables and charts
const liveFeedTable = document.getElementById('attack-tracking');
if (liveFeedTable) {
liveFeedTable.innerHTML = '';
}
// Clear data structures
this.ipStats = {};
this.countryStats = {};
this.countryTrackingStats = {};
this.protocolStats = {};
this.attackHistory = [];
// Reset Honeypot Stats
if (this.honeypotStats && this.honeypotStats.data) {
this.honeypotStats.data.clear();
}
// Reset Timeline Data
this.timelineData = this.initializeTimelineData();
// Update Tables
this.updateTopIPsTable();
this.updateTopCountriesTable();
// Reset Charts
// 1. Honeypot Chart
if (this.charts.honeypot) {
this.charts.honeypot.data.labels = ['Sin datos'];
this.charts.honeypot.data.datasets[0].data = [0];
if (this.charts.honeypot.data.datasets[0].originalData) {
this.charts.honeypot.data.datasets[0].originalData = [0];
}
this.charts.honeypot.update();
}
// 2. Protocol Chart
if (this.charts.protocol) {
this.charts.protocol.data.labels = [];
this.charts.protocol.data.datasets[0].data = [];
if (this.charts.protocol.data.datasets[0].originalData) {
this.charts.protocol.data.datasets[0].originalData = [];
}
this.charts.protocol.update();
}
// 3. Timeline Chart
if (this.charts.timeline) {
const labels = this.timelineData.map(d => d.label);
const data = this.timelineData.map(d => d.count);
this.charts.timeline.data.labels = labels;
this.charts.timeline.data.datasets[0].data = data;
this.charts.timeline.update();
}
// Reset attack statistics
this.stats = {
totalAttacks: 0,
uniqueAttackers: 0,
topCountries: [],
topProtocols: [],
recentActivity: []
};
// Update cache status indicator
this.updateCacheStatus();
this.showNotification('Caché borrada', 'success', 'cache');
} catch (error) {
console.error('[ERROR] Failed to clear cache:', error);
this.showNotification('Failed to clear cache', 'error', 'cache');
}
}
initSoundSystem() {
// Create audio context for sound alerts
this.audioContext = null;
this.soundBuffers = {};
this.audioInitialized = false;
// Add event listener for sound alerts checkbox
const soundAlerts = document.getElementById('sound-alerts');
if (soundAlerts) {
soundAlerts.addEventListener('change', () => {
// Update the settings object immediately
this.settings.soundAlerts = soundAlerts.checked;
// Save to localStorage
localStorage.setItem('attack-map-settings', JSON.stringify(this.settings));
// Apply visual changes
this.applySettings();
// Initialize audio when enabling sound
if (soundAlerts.checked && !this.audioInitialized) {
this.initializeAudioContext();
}
// Show feedback notification
this.showNotification(
soundAlerts.checked ? 'Sonido activado' : 'Sonido desactivado',
'success',
'sound'
);
});
}
// Add event listener for alert sound dropdown
const alertSound = document.getElementById('alert-sound');
if (alertSound) {
alertSound.addEventListener('change', () => {
// Update the settings object immediately
this.settings.alertSound = alertSound.value;
// Save to localStorage
localStorage.setItem('attack-map-settings', JSON.stringify(this.settings));
// Show feedback notification
this.showNotification(
`Alert sound changed to ${alertSound.options[alertSound.selectedIndex].text}`,
'success',
'sound'
);
});
}
// Initialize audio context on various user interactions
const initAudioOnInteraction = (event) => {
if (!this.audioInitialized) {
this.initializeAudioContext();
console.log('[SOUND] Audio initialized on:', event.type);
// Don't forcefully remove audio prompt - let it expire naturally after 5 seconds
}
};
// Add listeners for user interaction - be more aggressive
document.addEventListener('click', initAudioOnInteraction, { once: true });
document.addEventListener('keydown', initAudioOnInteraction, { once: true });
document.addEventListener('touchstart', initAudioOnInteraction, { once: true });
// Also initialize on any interaction with main content areas
const contentAreas = ['#map', '#dashboard', '#side-panel', '#bottom-panel'];
contentAreas.forEach(selector => {
const element = document.querySelector(selector);
if (element) {
element.addEventListener('click', initAudioOnInteraction, { once: true });
element.addEventListener('mouseover', initAudioOnInteraction, { once: true });
}
});
// Show audio prompt if sound is enabled but not initialized
this.showAudioPromptIfNeeded();
this.loadSoundEffects();
}
showAudioPromptIfNeeded() {
// Only show prompt if sound alerts are enabled and audio is not yet initialized
if (this.settings.soundAlerts && !this.audioInitialized) {
this.showNotification(
'🔊 Click anywhere to enable sound alerts',
'info',
'audio'
);
}
}
initializeAudioContext() {
try {
if (!this.audioContext) {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
// Resume audio context if it's suspended (some browsers suspend it by default)
if (this.audioContext.state === 'suspended') {
this.audioContext.resume().then(() => {
console.log('[SOUND] Audio context resumed');
this.audioInitialized = true;
}).catch(error => {
console.warn('[SOUND] Could not resume audio context:', error);
});
} else {
console.log('[SOUND] Audio context initialized, state:', this.audioContext.state);
this.audioInitialized = true;
}
} catch (error) {
console.warn('[SOUND] Could not initialize audio context:', error);
}
}
loadSoundEffects() {
// Create simple sound effects programmatically
this.createSoundEffects();
}
createSoundEffects() {
// We'll create simple beep sounds using Web Audio API
this.soundGenerators = {
beep: () => this.generateBeep(800, 0.1),
notification: () => this.generateChime([523, 659, 784], 0.15),
alert: () => this.generateAlert([400, 800, 400], 0.2),
retro_videogame: () => this.generateRetroVideogame()
};
}
generateBeep(frequency, duration) {
if (!this.audioContext) {
this.initializeAudioContext();
}
if (!this.audioContext) {
console.warn('[SOUND] Audio context not available');
return;
}
const oscillator = this.audioContext.createOscillator();
const gainNode = this.audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(this.audioContext.destination);
oscillator.frequency.setValueAtTime(frequency, this.audioContext.currentTime);
oscillator.type = 'sine';
gainNode.gain.setValueAtTime(0.1, this.audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration);
oscillator.start(this.audioContext.currentTime);
oscillator.stop(this.audioContext.currentTime + duration);
}
generateChime(frequencies, duration) {
if (!this.audioContext) {
this.initializeAudioContext();
}
if (!this.audioContext) {
console.warn('[SOUND] Audio context not available');
return;
}
frequencies.forEach((freq, index) => {
setTimeout(() => {
const oscillator = this.audioContext.createOscillator();
const gainNode = this.audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(this.audioContext.destination);
oscillator.frequency.setValueAtTime(freq, this.audioContext.currentTime);
oscillator.type = 'sine';
gainNode.gain.setValueAtTime(0.05, this.audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration);
oscillator.start(this.audioContext.currentTime);
oscillator.stop(this.audioContext.currentTime + duration);
}, index * 50);
});
}
generateAlert(frequencies, duration) {
if (!this.audioContext) {
this.initializeAudioContext();
}
if (!this.audioContext) {
console.warn('[SOUND] Audio context not available');
return;
}
frequencies.forEach((freq, index) => {
setTimeout(() => {
const oscillator = this.audioContext.createOscillator();
const gainNode = this.audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(this.audioContext.destination);
oscillator.frequency.setValueAtTime(freq, this.audioContext.currentTime);
oscillator.type = 'square';
gainNode.gain.setValueAtTime(0.08, this.audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration/3);
oscillator.start(this.audioContext.currentTime);
oscillator.stop(this.audioContext.currentTime + duration/3);
}, index * 100);
});
}
generateRetroVideogame() {
if (!this.audioContext) {
this.initializeAudioContext();
}
if (!this.audioContext) {
console.warn('[SOUND] Audio context not available');
return;
}
// Classic retro videogame style sound with descending frequency sweep
const startFreq = 220; // Starting frequency (A3)
const endFreq = 55; // Ending frequency (A1) - two octaves down
const duration = 0.3; // Total duration
const oscillator = this.audioContext.createOscillator();
const gainNode = this.audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(this.audioContext.destination);
// Use square wave for that classic 8-bit sound
oscillator.type = 'square';
// Create the characteristic descending frequency sweep
oscillator.frequency.setValueAtTime(startFreq, this.audioContext.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(endFreq, this.audioContext.currentTime + duration);
// Volume envelope: quick attack, then fade out
gainNode.gain.setValueAtTime(0.12, this.audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration);
oscillator.start(this.audioContext.currentTime);
oscillator.stop(this.audioContext.currentTime + duration);
// Add a second harmonic for richness (classic arcade sound technique)
setTimeout(() => {
const oscillator2 = this.audioContext.createOscillator();
const gainNode2 = this.audioContext.createGain();
oscillator2.connect(gainNode2);
gainNode2.connect(this.audioContext.destination);
oscillator2.type = 'square';
// Second oscillator at a higher frequency for harmonic content
oscillator2.frequency.setValueAtTime(startFreq * 1.5, this.audioContext.currentTime);
oscillator2.frequency.exponentialRampToValueAtTime(endFreq * 1.5, this.audioContext.currentTime + duration * 0.6);
// Lower volume for the harmonic
gainNode2.gain.setValueAtTime(0.06, this.audioContext.currentTime);
gainNode2.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration * 0.6);
oscillator2.start(this.audioContext.currentTime);
oscillator2.stop(this.audioContext.currentTime + duration * 0.6);
}, 20); // Slight delay for phasing effect
}
playAlertSound() {
if (!this.settings.soundAlerts) {
return;
}
// Try to initialize audio context if not done yet
if (!this.audioInitialized) {
this.initializeAudioContext();
}
if (!this.soundGenerators) {
console.warn('[SOUND] Sound generators not available');
return;
}
const soundType = this.settings.alertSound || 'beep';
const generator = this.soundGenerators[soundType];
if (generator) {
try {
generator();
console.log('[SOUND] Played sound:', soundType);
} catch (error) {
console.warn('[SOUND] Could not play sound:', error);
// Try to reinitialize audio context on error
if (!this.audioInitialized) {
this.initializeAudioContext();
}
}
} else {
console.warn('[SOUND] Sound generator not found for:', soundType);
}
}
openSettings() {
const modal = document.getElementById('settings-modal');
if (modal) {
modal.classList.add('active');
}
}
closeSettings() {
const modal = document.getElementById('settings-modal');
if (modal) {
modal.classList.remove('active');
}
}
// Utility Functions
toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(err => {
console.log(`Error attempting to enable fullscreen: ${err.message}`);
});
} else {
document.exitFullscreen();
}
}
handleKeyboardShortcuts(e) {
// Ctrl/Cmd + key combinations
if (e.ctrlKey || e.metaKey) {
switch (e.key) {
case 'f':
e.preventDefault();
this.toggleFullscreen();
break;
case 't':
e.preventDefault();
this.toggleTheme();
break;
case ',':
e.preventDefault();
this.openSettings();
break;
}
}
// Escape key
if (e.key === 'Escape') {
this.closeSettings();
}
// Tab navigation
if (e.key >= '1' && e.key <= '4' && e.altKey) {
e.preventDefault();
const tabs = ['live-feed', 'top-ips', 'countries', 'overview'];
const index = parseInt(e.key) - 1;
if (tabs[index]) {
this.switchTab(tabs[index]);
}
}
}
handleWindowResize() {
this.resizeCharts();
this.updateMapSize();
}
startPerformanceMonitoring() {
setInterval(() => {
// Monitor performance and adjust if needed
// performance.memory is a non-standard API (Chrome only)
if (window.performance && window.performance.memory) {
const memInfo = window.performance.memory;
if (memInfo.usedJSHeapSize > 100 * 1024 * 1024) { // 100MB
this.optimizeMemoryUsage();
}
}
}, 30000); // Check every 30 seconds
}
startConnectionStatusMonitoring() {
// Check if WebSocket was already connected before dashboard loaded
if (window.webSocketConnected === true) {
console.log('[*] Dashboard initialized - WebSocket already connected, setting status to connected');
this.updateConnectionStatus('connected');
} else {
console.log('[*] Dashboard initialized - WebSocket not yet connected, setting status to connecting');
// Set initial status to connecting
this.updateConnectionStatus('connecting');
}
// Periodically check and sync connection status
setInterval(() => {
this.syncConnectionStatus();
}, 2000); // Check every 2 seconds
}
syncConnectionStatus() {
const now = Date.now();
const IDLE_THRESHOLD = 30000; // 30 seconds
// 1. Check WebSocket State
if (!window.webSocket) {
this.updateConnectionStatus('disconnected');
return;
}
const state = window.webSocket.readyState;
if (state === WebSocket.CONNECTING) {
this.updateConnectionStatus('connecting');
return;
}
if (state === WebSocket.CLOSING || state === WebSocket.CLOSED) {
this.updateConnectionStatus('disconnected');
return;
}
// 2. Socket is OPEN. Check Data Recency.
// Use window.lastValidDataTime which tracks actual data events
const lastMsgTime = window.lastValidDataTime || 0;
const timeSinceLastMsg = now - lastMsgTime;
if (timeSinceLastMsg < IDLE_THRESHOLD) {
this.updateConnectionStatus('connected');
} else {
this.updateConnectionStatus('idle');
}
}
optimizeMemoryUsage() {
// Limit attack history to match cache size
if (this.attackHistory.length > this.attackCache.maxEvents) {
this.attackHistory = this.attackHistory.slice(-this.attackCache.maxEvents);
}
// Limit table rows
const tables = ['ip-tracking', 'country-tracking', 'attack-tracking'];
tables.forEach(tableId => {
const tbody = document.getElementById(tableId);
if (tbody && tbody.children.length > 100) {
while (tbody.children.length > 50) {
tbody.removeChild(tbody.lastChild);
}
}
});
}
startDataAggregation() {
// Update timeline with different frequencies based on interval
this.startTimelineUpdates();
// Update other stats every 5 seconds (synchronized updates)
setInterval(() => {
this.aggregateProtocolStats();
this.aggregateCountryStats();
this.updateDashboardMetrics();
this.updateHoneypotChartData(); // Synchronized with other card updates
}, 5000); // Every 5 seconds
}
// Start timeline updates with appropriate frequency
startTimelineUpdates() {
if (this.timelineUpdateInterval) {
clearInterval(this.timelineUpdateInterval);
}
let updateFrequency;
switch (this.timelineInterval) {
case '1m':
updateFrequency = 1000; // Update every second
break;
case '1h':
updateFrequency = 5000; // Update every 5 seconds
break;
case '24h':
default:
updateFrequency = 30000; // Update every 30 seconds
break;
}
this.timelineUpdateInterval = setInterval(() => {
this.updateTimeline();
}, updateFrequency);
console.log(`[DEBUG] Timeline updates started with ${updateFrequency}ms interval for ${this.timelineInterval} mode`);
}
aggregateProtocolStats() {
// Use consistent time window for protocol stats
const retentionMinutes = 15;
const retentionTime = retentionMinutes * 60 * 1000; // 15 minutes
const recent = this.attackHistory.filter(attack =>
Date.now() - attack.timestamp < retentionTime
);
this.protocolStats = recent.reduce((stats, attack) => {
const normalizedProtocol = this.normalizeProtocol(attack.protocol);
stats[normalizedProtocol] = (stats[normalizedProtocol] || 0) + 1;
return stats;
}, {});
// Update data retention display
this.updateDataRetentionInfo('attack-distribution', retentionMinutes);
this.updateDataRetentionInfo('protocol-breakdown', retentionMinutes);
}
aggregateCountryStats() {
// Use consistent time window for country stats
const retentionMinutes = 15;
const retentionTime = retentionMinutes * 60 * 1000; // 15 minutes
const recent = this.attackHistory.filter(attack =>
Date.now() - attack.timestamp < retentionTime
);
this.countryStats = recent.reduce((stats, attack) => {
stats[attack.country] = (stats[attack.country] || 0) + 1;
return stats;
}, {});
}
// Add method to update data retention information display
updateDataRetentionInfo(cardType, retentionMinutes) {
const cards = document.querySelectorAll('.dashboard-card');
cards.forEach(card => {
const header = card.querySelector('.card-header h4');
if (header) {
const text = header.textContent;
if ((cardType === 'attack-distribution' && text.includes('Attack Distribution')) ||
(cardType === 'protocol-breakdown' && text.includes('Protocol Breakdown'))) {
// Remove existing retention info
let retentionSpan = card.querySelector('.data-retention-info');
if (retentionSpan) {
retentionSpan.remove();
}
// Add new retention info
retentionSpan = document.createElement('span');
retentionSpan.className = 'data-retention-info';
retentionSpan.textContent = ` (Last ${retentionMinutes}m)`;
retentionSpan.style.fontSize = '0.8em';
retentionSpan.style.color = 'var(--text-secondary)';
retentionSpan.style.fontWeight = 'normal';
header.appendChild(retentionSpan);
}
}
});
}
updateDashboardMetrics() {
// Update various dashboard metrics
if (this.activeTab === 'overview') {
this.updateOverviewCharts();
}
// Note: Header stats are updated by WebSocket Stats messages (handleStats in map.js)
// which contain correct historical data from Elasticsearch
}
hideLoadingScreen() {
setTimeout(() => {
const loadingScreen = document.getElementById('loading-screen');
if (loadingScreen) {
loadingScreen.classList.add('hidden');
}
}, 1500);
}
showNotification(message, type = 'info', context = 'general') {
// Ensure notification container exists and is properly configured
let container = document.getElementById('notification-container');
if (!container) {
container = document.createElement('div');
container.id = 'notification-container';
document.body.appendChild(container);
}
// Create notification element
const notification = document.createElement('div');
notification.className = `notification ${type}`;
// Add context-specific class if needed
if (context !== 'general') {
notification.classList.add(`notification-${context}`);
}
// Create notification structure using DOM API
const header = document.createElement('div');
header.className = 'notification-header';
const title = document.createElement('div');
title.className = 'notification-title';
title.textContent = this.getNotificationTitle(type);
header.appendChild(title);
const closeBtn = document.createElement('button');
closeBtn.className = 'notification-close';
closeBtn.innerHTML = '&times;'; // Safe entity
header.appendChild(closeBtn);
notification.appendChild(header);
const msgDiv = document.createElement('div');
msgDiv.className = 'notification-message';
msgDiv.textContent = message;
notification.appendChild(msgDiv);
const timeDiv = document.createElement('div');
timeDiv.className = 'notification-timestamp';
timeDiv.textContent = new Date().toLocaleTimeString();
notification.appendChild(timeDiv);
// Add close button event listener
const closeButton = notification.querySelector('.notification-close');
closeButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
if (notification.parentNode) {
notification.classList.add('fade-out');
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 300);
}
});
// Add to container at the top (newest first)
container.insertBefore(notification, container.firstChild);
// Force reflow to ensure proper positioning
notification.offsetHeight;
// Auto-remove after 5 seconds
setTimeout(() => {
if (notification.parentNode) {
notification.classList.add('fade-out');
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 300);
}
}, 5000);
}
getNotificationTitle(type) {
const titles = {
'success': 'Success',
'info': 'Information',
'warning': 'Warning',
'error': 'Error'
};
return titles[type] || 'Notification';
}
// Public API for external components
addAttackEvent(event) {
console.log('[DEBUG] Dashboard received attack event:', event);
event.timestamp = Date.now();
this.attackHistory.push(event);
// Store in cache if initialized and not currently restoring
if (this.cacheInitialized && !this.restoringFromCache) {
this.attackCache.storeEvent(event).catch(error => {
console.warn('[CACHE] Failed to store event:', error);
});
}
// Process attack for honeypot tracking if honeypot field is present
if (event.honeypot) {
this.trackHoneypotAttack(event.honeypot, event.timestamp);
}
// Try to initialize audio context if not already done (aggressive approach)
if (!this.audioInitialized) {
console.log('[SOUND] Attempting audio initialization on attack event');
this.initializeAudioContext();
}
// Play sound alert for new attack
this.playAlertSound();
// Add to timeline data in real-time
this.addAttackToTimeline(event);
// Add to heatmap data in real-time
this.addAttackToHeatmap(event);
// Limit history size
if (this.attackHistory.length > 1000) {
this.attackHistory.shift();
}
// Update relevant displays (synchronized updates)
this.updateLiveAttackDisplay(event);
this.updateHoneypotChartData(); // Update honeypot chart in sync with other cards
console.log('[DEBUG] Attack event processed, history length:', this.attackHistory.length);
}
updateConnectionStatus(status) {
// Prevent unnecessary updates
if (this.connectionStatus === status) {
return;
}
const indicator = document.getElementById('status-indicator');
const text = document.getElementById('status-text');
console.log(`[DEBUG] Connection status update: ${this.connectionStatus} -> ${status}`);
if (indicator && text) {
const oldStatus = indicator.className;
indicator.className = `status-indicator ${status}`;
switch (status) {
case 'connected':
text.textContent = 'Conectado';
console.log('[*] Status indicator set to Connected');
break;
case 'idle':
text.textContent = 'Inactivo';
console.log('[*] Status indicator set to Idle');
break;
case 'connecting':
text.textContent = 'Conectando...';
console.log('[*] Status indicator set to Connecting...');
break;
case 'disconnected':
default:
text.textContent = 'Desconectado';
break;
}
this.connectionStatus = status;
console.log(`[DEBUG] Connection status UI updated from '${oldStatus}' to '${indicator.className}'`);
} else {
console.warn('[WARNING] Connection status elements not found');
}
}
// Helper method to update IP tracking data for restored events
updateIPTracking(ip, country, event) {
if (!ip) return;
if (!this.ipStats.has(ip)) {
this.ipStats.set(ip, { count: 0, country: country || 'Unknown', firstSeen: event.timestamp });
}
const stats = this.ipStats.get(ip);
stats.count++;
stats.lastSeen = event.timestamp;
}
// Helper method to update country tracking data for restored events
updateCountryTracking(country, event) {
if (!country || country === 'Unknown') return;
if (!this.countryStats.has(country)) {
this.countryStats.set(country, { count: 0, firstSeen: event.timestamp });
}
const stats = this.countryStats.get(country);
stats.count++;
stats.lastSeen = event.timestamp;
}
// Helper method to update protocol stats for restored events
updateProtocolStats(protocol) {
if (!protocol) return;
this.protocolStats[protocol] = (this.protocolStats[protocol] || 0) + 1;
}
addToLiveFeed(event) {
// Add event to live feed table without highlighting (for cache restoration)
this.addToAttackTable(event, false); // false = no highlighting for restored events
// Update IP tracking (needed for top IPs table)
this.updateIPTracking(event.ip || event.source_ip, event.country, event);
// Update country tracking (needed for top countries table)
this.updateCountryTracking(event.country, event);
// Update protocol statistics (needed for protocol breakdown)
this.updateProtocolStats(event.protocol);
}
updateLiveAttackDisplay(event) {
// Update live attack feed table
this.addToAttackTable(event);
// Update IP tracking
this.updateIPTracking(event.ip, event.country, event);
// Update country tracking
this.updateCountryTracking(event.country, event);
// Update protocol statistics
this.updateProtocolStats(event.protocol);
// Update real-time charts if on overview tab
if (this.activeTab === 'overview') {
this.updateOverviewCharts();
}
}
addToAttackTable(event, highlight = true) {
const tbody = document.getElementById('attack-tracking');
if (!tbody) {
console.warn('[WARNING] attack-tracking table not found');
return;
}
const row = document.createElement('tr');
// Add the new row highlight class only for real-time events
if (highlight) {
row.classList.add('new-attack-row');
// Remove the highlight class after animation completes
setTimeout(() => {
row.classList.remove('new-attack-row');
}, 2000);
}
// Use the event timestamp if available, otherwise current time
const eventTime = event.timestamp ? new Date(event.timestamp) : new Date();
// Format timestamp as YYYY-MM-DD HH:MM:SS
const year = eventTime.getFullYear();
const month = String(eventTime.getMonth() + 1).padStart(2, '0');
const day = String(eventTime.getDate()).padStart(2, '0');
const hours = String(eventTime.getHours()).padStart(2, '0');
const minutes = String(eventTime.getMinutes()).padStart(2, '0');
const seconds = String(eventTime.getSeconds()).padStart(2, '0');
const timeDisplay = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
// Use ISO code for flag path (should be 2-letter uppercase code)
const flagCode = event.iso_code || event.country_code || 'XX';
// Determine protocol - use "OTHER" if not specified or unknown
const protocolName = this.normalizeProtocol(event.protocol);
const protocol = protocolName.toLowerCase();
const protocolClass = `protocol-${protocol}`;
// Create cells using DOM API to prevent XSS
const addCell = (className, text) => {
const td = document.createElement('td');
td.className = className;
td.textContent = text;
row.appendChild(td);
return td;
};
addCell('time-cell', timeDisplay);
addCell('ip-cell', event.ip || event.src_ip || 'Unknown');
addCell('ip-rep-cell', event.ip_rep || 'Unknown');
const flagCell = document.createElement('td');
flagCell.className = 'flag-cell';
const flagImg = document.createElement('img');
flagImg.src = `static/flags/${flagCode}.svg`;
flagImg.alt = event.country || 'Unknown';
flagImg.className = 'flag-icon';
flagImg.onerror = function() { this.src = 'static/flags/XX.svg'; };
flagCell.appendChild(flagImg);
row.appendChild(flagCell);
addCell('country-cell', event.country || 'Unknown');
addCell('honeypot-cell', event.honeypot || 'Unknown');
const protocolCell = document.createElement('td');
protocolCell.className = 'protocol-cell';
const badge = document.createElement('span');
badge.className = `protocol-badge ${protocolClass}`;
badge.textContent = protocolName;
protocolCell.appendChild(badge);
row.appendChild(protocolCell);
addCell('port-cell', event.port || event.dst_port || 'N/A');
addCell('tpot-hostname-cell', event.tpot_hostname || 'Unknown');
// Add to top of table
tbody.insertBefore(row, tbody.firstChild);
// Limit table size
while (tbody.children.length > 100) {
tbody.removeChild(tbody.lastChild);
}
console.log('[DEBUG] Added row to attack table for IP:', event.ip || event.src_ip);
}
updateIPTracking(ip, country, event = null) {
// Update IP statistics
if (!this.ipStats) this.ipStats = {};
// Determine timestamp to use
const timestamp = (event && event.timestamp) ? new Date(event.timestamp) : new Date();
if (!this.ipStats[ip]) {
this.ipStats[ip] = {
hits: 0,
country: country,
lastSeen: timestamp,
ip: ip,
reputation: 'Unknown',
lastProtocol: 'Unknown',
countryCode: 'XX' // Store ISO code
};
}
this.ipStats[ip].hits++;
// Update lastSeen if the event is newer than what we have
if (timestamp >= this.ipStats[ip].lastSeen) {
this.ipStats[ip].lastSeen = timestamp;
}
this.ipStats[ip].country = country; // Update country in case it changes
// Update reputation, protocol and country code if event data is available
if (event) {
if (event.ip_rep) {
this.ipStats[ip].reputation = event.ip_rep;
}
if (event.protocol) {
this.ipStats[ip].lastProtocol = this.normalizeProtocol(event.protocol);
}
// Store the actual ISO code from Elasticsearch
if (event.iso_code || event.country_code) {
this.ipStats[ip].countryCode = event.iso_code || event.country_code;
}
}
// Update IP table if it's the active tab
if (this.activeTab === 'top-ips') {
this.updateTopIPsTable();
}
}
updateCountryTracking(country, event = null) {
// Update country statistics
if (!this.countryTrackingStats) this.countryTrackingStats = {};
// Determine timestamp to use
const timestamp = (event && event.timestamp) ? new Date(event.timestamp) : new Date();
if (!this.countryTrackingStats[country]) {
this.countryTrackingStats[country] = {
hits: 0,
country: country,
lastSeen: timestamp,
topProtocol: 'Unknown',
protocolCounts: {},
uniqueIPs: new Set(),
lastSeenIP: 'Unknown',
countryCode: 'XX' // Store ISO code
};
}
this.countryTrackingStats[country].hits++;
// Update lastSeen if the event is newer than what we have
if (timestamp >= this.countryTrackingStats[country].lastSeen) {
this.countryTrackingStats[country].lastSeen = timestamp;
}
// Update additional fields if event data is available
if (event) {
// Store the actual ISO code from Elasticsearch
if (event.iso_code || event.country_code) {
this.countryTrackingStats[country].countryCode = event.iso_code || event.country_code;
}
// Track unique IPs for this country
if (event.ip) {
this.countryTrackingStats[country].uniqueIPs.add(event.ip);
this.countryTrackingStats[country].lastSeenIP = event.ip;
}
// Track protocol counts to determine top protocol
if (event.protocol) {
const normalizedProtocol = this.normalizeProtocol(event.protocol);
if (!this.countryTrackingStats[country].protocolCounts[normalizedProtocol]) {
this.countryTrackingStats[country].protocolCounts[normalizedProtocol] = 0;
}
this.countryTrackingStats[country].protocolCounts[normalizedProtocol]++;
// Update top protocol (most frequent)
let maxCount = 0;
let topProtocol = 'Unknown';
for (const [protocol, count] of Object.entries(this.countryTrackingStats[country].protocolCounts)) {
if (count > maxCount) {
maxCount = count;
topProtocol = protocol;
}
}
this.countryTrackingStats[country].topProtocol = topProtocol;
}
}
// Update country table if it's the active tab
if (this.activeTab === 'countries') {
this.updateTopCountriesTable();
}
}
updateTopIPsTable() {
const tbody = document.getElementById('ip-tracking');
if (!tbody || !this.ipStats) return;
// Sort IPs by hits (descending)
const sortedIPs = Object.values(this.ipStats)
.sort((a, b) => b.hits - a.hits)
.slice(0, 100); // Top 100
tbody.innerHTML = '';
sortedIPs.forEach((ipData, index) => {
const row = document.createElement('tr');
// Format timestamp as YYYY-MM-DD HH:MM:SS
const eventTime = ipData.lastSeen;
const year = eventTime.getFullYear();
const month = String(eventTime.getMonth() + 1).padStart(2, '0');
const day = String(eventTime.getDate()).padStart(2, '0');
const hours = String(eventTime.getHours()).padStart(2, '0');
const minutes = String(eventTime.getMinutes()).padStart(2, '0');
const seconds = String(eventTime.getSeconds()).padStart(2, '0');
const timeDisplay = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
// Use stored ISO code instead of converting country name
const flagCode = ipData.countryCode || 'XX';
// Protocol badge styling
const protocol = ipData.lastProtocol.toLowerCase();
const protocolClass = `protocol-${protocol}`;
// Create cells using DOM API to prevent XSS
const addCell = (className, text) => {
const td = document.createElement('td');
td.className = className;
td.textContent = text;
row.appendChild(td);
return td;
};
addCell('rank-cell', index + 1);
addCell('hits-cell', ipData.hits);
addCell('ip-cell', ipData.ip);
addCell('ip-rep-cell', ipData.reputation);
const flagCell = document.createElement('td');
flagCell.className = 'flag-cell';
const flagImg = document.createElement('img');
flagImg.src = `static/flags/${flagCode}.svg`;
flagImg.alt = ipData.country || 'Unknown';
flagImg.className = 'flag-icon';
flagImg.onerror = function() { this.src = 'static/flags/XX.svg'; };
flagCell.appendChild(flagImg);
row.appendChild(flagCell);
addCell('country-cell', ipData.country || 'Unknown');
const protocolCell = document.createElement('td');
protocolCell.className = 'protocol-cell';
const badge = document.createElement('span');
badge.className = `protocol-badge ${protocolClass}`;
badge.textContent = ipData.lastProtocol;
protocolCell.appendChild(badge);
row.appendChild(protocolCell);
addCell('time-cell', timeDisplay);
tbody.appendChild(row);
});
}
updateTopCountriesTable() {
const tbody = document.getElementById('country-tracking');
if (!tbody || !this.countryTrackingStats) return;
// Sort countries by hits (descending)
const sortedCountries = Object.values(this.countryTrackingStats)
.sort((a, b) => b.hits - a.hits)
.slice(0, 100); // Top 100
tbody.innerHTML = '';
sortedCountries.forEach((countryData, index) => {
const row = document.createElement('tr');
// Format timestamp as YYYY-MM-DD HH:MM:SS
const eventTime = countryData.lastSeen;
const year = eventTime.getFullYear();
const month = String(eventTime.getMonth() + 1).padStart(2, '0');
const day = String(eventTime.getDate()).padStart(2, '0');
const hours = String(eventTime.getHours()).padStart(2, '0');
const minutes = String(eventTime.getMinutes()).padStart(2, '0');
const seconds = String(eventTime.getSeconds()).padStart(2, '0');
const timeDisplay = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
// Use stored ISO code instead of converting country name
const flagCode = countryData.countryCode || 'XX';
// Protocol badge styling
const protocol = countryData.topProtocol.toLowerCase();
const protocolClass = `protocol-${protocol}`;
// Get unique IP count
const uniqueIPCount = countryData.uniqueIPs ? countryData.uniqueIPs.size : 0;
// Create cells using DOM API to prevent XSS
const addCell = (className, text) => {
const td = document.createElement('td');
td.className = className;
td.textContent = text;
row.appendChild(td);
return td;
};
addCell('rank-cell', index + 1);
addCell('hits-cell', countryData.hits);
const flagCell = document.createElement('td');
flagCell.className = 'flag-cell';
const flagImg = document.createElement('img');
flagImg.src = `static/flags/${flagCode}.svg`;
flagImg.alt = countryData.country || 'Unknown';
flagImg.className = 'flag-icon';
flagImg.onerror = function() { this.src = 'static/flags/XX.svg'; };
flagCell.appendChild(flagImg);
row.appendChild(flagCell);
addCell('country-cell', countryData.country || 'Unknown');
const protocolCell = document.createElement('td');
protocolCell.className = 'protocol-cell';
const badge = document.createElement('span');
badge.className = `protocol-badge ${protocolClass}`;
badge.textContent = countryData.topProtocol;
protocolCell.appendChild(badge);
row.appendChild(protocolCell);
addCell('unique-ips-cell', uniqueIPCount);
addCell('last-ip-cell', countryData.lastSeenIP);
addCell('time-cell', timeDisplay);
tbody.appendChild(row);
});
}
updateProtocolStats(protocol) {
if (!protocol) return;
// Normalize protocol to known protocols or "OTHER"
const normalizedProtocol = this.normalizeProtocol(protocol);
this.protocolStats[normalizedProtocol] = (this.protocolStats[normalizedProtocol] || 0) + 1;
// Update protocol chart if it exists
if (this.charts.protocol) {
const labels = Object.keys(this.protocolStats);
const data = Object.values(this.protocolStats);
this.charts.protocol.data.labels = labels;
this.charts.protocol.data.datasets[0].data = data.map(d => Math.sqrt(d));
this.charts.protocol.data.datasets[0].originalData = data;
this.charts.protocol.update('none'); // Update without animation for performance
}
}
initThreatHeatmap() {
const container = document.getElementById('threat-heatmap');
if (!container) return;
// Create heatmap grid
container.innerHTML = `
<div class="heatmap-grid">
<div class="heatmap-content">
<div class="heatmap-timeline">
<div class="timeline-grid"></div>
<div class="timeline-labels"></div>
</div>
</div>
<div class="heatmap-legend">
<span class="legend-label">Low</span>
<div class="legend-gradient"></div>
<span class="legend-label">High</span>
</div>
</div>
`;
this.setupHeatmapData();
this.updateThreatHeatmap();
}
setupHeatmapData() {
// Initialize 24 hours of heatmap data with real data
this.heatmapData = Array.from({ length: 24 }, (_, hour) => ({
hour: hour,
intensity: 0, // Will be calculated from real attack data
attacks: 0 // Will be calculated from real attack data
}));
// Populate with real attack data
this.populateHeatmapFromHistory();
}
// Populate heatmap with real attack data from history
populateHeatmapFromHistory() {
if (!this.attackHistory.length) {
console.log(`[DEBUG] No attack history available for heatmap population`);
return;
}
console.log(`[DEBUG] Populating heatmap from ${this.attackHistory.length} attacks`);
// Reset counts
this.heatmapData.forEach(hourData => {
hourData.attacks = 0;
hourData.intensity = 0;
});
const now = Date.now();
const last24Hours = now - (24 * 60 * 60 * 1000); // 24 hours ago
// Filter attacks from last 24 hours
const recentAttacks = this.attackHistory.filter(attack =>
attack.timestamp >= last24Hours
);
console.log(`[DEBUG] Found ${recentAttacks.length} attacks in last 24 hours for heatmap`);
// Count attacks per hour
recentAttacks.forEach(attack => {
const attackTime = new Date(attack.timestamp);
const hour = attackTime.getHours();
if (hour >= 0 && hour <= 23) {
this.heatmapData[hour].attacks++;
}
});
// Calculate intensity based on attack counts
const maxAttacks = Math.max(...this.heatmapData.map(h => h.attacks), 1);
this.heatmapData.forEach(hourData => {
hourData.intensity = maxAttacks > 0 ? (hourData.attacks / maxAttacks) * 100 : 0;
});
console.log(`[DEBUG] Heatmap populated - attacks per hour:`, this.heatmapData.map(h => h.attacks));
}
// Add attack to heatmap data
addAttackToHeatmap(attack, triggerUpdate = true) {
const attackTime = new Date(attack.timestamp || Date.now());
const hour = attackTime.getHours();
if (hour >= 0 && hour <= 23) {
this.heatmapData[hour].attacks++;
// Recalculate intensity
const maxAttacks = Math.max(...this.heatmapData.map(h => h.attacks), 1);
this.heatmapData.forEach(hourData => {
hourData.intensity = maxAttacks > 0 ? (hourData.attacks / maxAttacks) * 100 : 0;
});
// Update heatmap display only if not restoring
if (triggerUpdate) {
this.updateThreatHeatmap();
}
}
}
updateThreatHeatmap() {
const timelineGrid = document.querySelector('.timeline-grid');
const timelineLabels = document.querySelector('.timeline-labels');
if (!timelineGrid || !timelineLabels) return;
// Create hour labels
timelineLabels.innerHTML = '';
for (let i = 0; i < 24; i += 3) {
const label = document.createElement('div');
label.className = 'timeline-label';
label.textContent = `${i.toString().padStart(2, '0')}:00`;
timelineLabels.appendChild(label);
}
// Create heatmap cells
timelineGrid.innerHTML = '';
this.heatmapData.forEach((data, index) => {
const cell = document.createElement('div');
cell.className = 'heatmap-cell';
cell.title = `${data.hour}:00 - ${data.attacks} ataques`;
cell.style.backgroundColor = this.getHeatmapColor(data.intensity);
cell.addEventListener('click', () => {
this.showHeatmapDetails(data);
});
timelineGrid.appendChild(cell);
});
}
getHeatmapColor(intensity) {
// Greyish/blue tone color gradient: Light Grey (safe) to Dark Blue (danger)
const ratio = intensity / 100;
if (ratio <= 0.25) {
// Light Grey to Light Blue (0-25%) - Safe/Low threat
const localRatio = ratio / 0.25;
const r = Math.floor(240 - localRatio * 65); // 240 -> 175
const g = Math.floor(240 - localRatio * 25); // 240 -> 215
const b = Math.floor(240 - localRatio * 15); // 240 -> 225
return `rgba(${r}, ${g}, ${b}, 0.85)`;
} else if (ratio <= 0.5) {
// Light Blue to Medium Blue (25-50%) - Moderate threat
const localRatio = (ratio - 0.25) / 0.25;
const r = Math.floor(175 - localRatio * 75); // 175 -> 100
const g = Math.floor(215 - localRatio * 65); // 215 -> 150
const b = Math.floor(225 - localRatio * 25); // 225 -> 200
return `rgba(${r}, ${g}, ${b}, 0.85)`;
} else if (ratio <= 0.75) {
// Medium Blue to Dark Blue (50-75%) - High threat
const localRatio = (ratio - 0.5) / 0.25;
const r = Math.floor(100 - localRatio * 50); // 100 -> 50
const g = Math.floor(150 - localRatio * 70); // 150 -> 80
const b = Math.floor(200 - localRatio * 40); // 200 -> 160
return `rgba(${r}, ${g}, ${b}, 0.85)`;
} else {
// Dark Blue to Navy Blue (75-100%) - Critical threat
const localRatio = (ratio - 0.75) / 0.25;
const r = Math.floor(50 - localRatio * 30); // 50 -> 20
const g = Math.floor(80 - localRatio * 50); // 80 -> 30
const b = Math.floor(160 - localRatio * 60); // 160 -> 100
return `rgba(${r}, ${g}, ${b}, 0.9)`;
}
}
showHeatmapDetails(data) {
// Create and show tooltip near the heatmap
const heatmapContainer = document.getElementById('threat-heatmap');
if (!heatmapContainer) return;
// Remove existing tooltip
const existingTooltip = document.querySelector('.heatmap-tooltip');
if (existingTooltip) {
existingTooltip.remove();
}
// Create new tooltip
const tooltip = document.createElement('div');
tooltip.className = 'heatmap-tooltip';
const strong = document.createElement('strong');
strong.textContent = `Hour ${data.hour}:00`;
tooltip.appendChild(strong);
tooltip.appendChild(document.createElement('br'));
tooltip.appendChild(document.createTextNode(`${data.attacks} ataques`));
tooltip.appendChild(document.createElement('br'));
tooltip.appendChild(document.createTextNode(`${Math.round(data.intensity)}% intensidad`));
// Position tooltip near the heatmap
const rect = heatmapContainer.getBoundingClientRect();
tooltip.style.cssText = `
position: fixed;
left: ${rect.left + 10}px;
top: ${rect.bottom - 80}px;
background: var(--bg-modal);
color: var(--text-primary);
padding: var(--spacing-sm);
border-radius: var(--radius-md);
border: 1px solid var(--border-primary);
box-shadow: 0 4px 16px var(--shadow-medium);
z-index: 10001;
font-size: var(--font-xs);
pointer-events: none;
`;
document.body.appendChild(tooltip);
// Auto-remove after 3 seconds
setTimeout(() => {
if (tooltip && tooltip.parentNode) {
tooltip.remove();
}
}, 3000);
}
}
// Initialize dashboard when DOM is loaded
// Initialize dashboard when DOM and scripts are loaded
window.addEventListener('load', () => {
// Wait for Chart.js to be available
function initWhenReady() {
if (typeof Chart !== 'undefined') {
console.log('[DEBUG] Chart.js available, initializing Attack Map Dashboard...');
window.attackMapDashboard = new AttackMapDashboard();
console.log('[DEBUG] Dashboard initialized:', window.attackMapDashboard);
} else {
console.log('[DEBUG] Chart.js not yet available, waiting...');
setTimeout(initWhenReady, 100);
}
}
initWhenReady();
});
// Export for use in other modules
window.AttackMapDashboard = AttackMapDashboard;