448 lines
15 KiB
JavaScript
448 lines
15 KiB
JavaScript
// WebSocket functionality for real-time updates
|
|
|
|
class WebSocketManager {
|
|
constructor() {
|
|
this.socket = null;
|
|
this.reconnectAttempts = 0;
|
|
this.maxReconnectAttempts = 5;
|
|
this.reconnectDelay = 1000;
|
|
this.isConnected = false;
|
|
this.messageHandlers = new Map();
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.connect();
|
|
this.setupEventListeners();
|
|
}
|
|
|
|
connect() {
|
|
try {
|
|
// Initialize Socket.IO connection
|
|
this.socket = io('/logs', {
|
|
transports: ['websocket', 'polling'],
|
|
upgrade: true,
|
|
rememberUpgrade: true
|
|
});
|
|
|
|
this.setupSocketEvents();
|
|
} catch (error) {
|
|
console.error('Failed to initialize WebSocket connection:', error);
|
|
this.scheduleReconnect();
|
|
}
|
|
}
|
|
|
|
setupSocketEvents() {
|
|
this.socket.on('connect', () => {
|
|
console.log('WebSocket connected');
|
|
this.isConnected = true;
|
|
this.reconnectAttempts = 0;
|
|
this.updateConnectionStatus('connected');
|
|
|
|
// Emit any pending events
|
|
this.emitPendingEvents();
|
|
});
|
|
|
|
this.socket.on('disconnect', (reason) => {
|
|
console.log('WebSocket disconnected:', reason);
|
|
this.isConnected = false;
|
|
this.updateConnectionStatus('disconnected');
|
|
|
|
// Attempt to reconnect unless it was a manual disconnect
|
|
if (reason !== 'io client disconnect') {
|
|
this.scheduleReconnect();
|
|
}
|
|
});
|
|
|
|
this.socket.on('connect_error', (error) => {
|
|
console.error('WebSocket connection error:', error);
|
|
this.isConnected = false;
|
|
this.updateConnectionStatus('error');
|
|
this.scheduleReconnect();
|
|
});
|
|
|
|
// Script execution events
|
|
this.socket.on('script_started', (data) => {
|
|
this.handleScriptEvent('started', data);
|
|
});
|
|
|
|
this.socket.on('script_completed', (data) => {
|
|
this.handleScriptEvent('completed', data);
|
|
});
|
|
|
|
this.socket.on('script_failed', (data) => {
|
|
this.handleScriptEvent('failed', data);
|
|
});
|
|
|
|
this.socket.on('script_output', (data) => {
|
|
this.handleScriptOutput(data);
|
|
});
|
|
|
|
// System events
|
|
this.socket.on('system_notification', (data) => {
|
|
this.handleSystemNotification(data);
|
|
});
|
|
|
|
// Custom message handling
|
|
this.socket.on('custom_message', (data) => {
|
|
this.handleCustomMessage(data);
|
|
});
|
|
}
|
|
|
|
scheduleReconnect() {
|
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
this.reconnectAttempts++;
|
|
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
|
|
console.log(`Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
|
|
|
setTimeout(() => {
|
|
this.connect();
|
|
}, delay);
|
|
} else {
|
|
console.error('Max reconnection attempts reached');
|
|
this.updateConnectionStatus('failed');
|
|
}
|
|
}
|
|
|
|
updateConnectionStatus(status) {
|
|
// Update UI connection indicator
|
|
const statusIndicator = document.getElementById('connection-status');
|
|
if (statusIndicator) {
|
|
statusIndicator.className = `connection-status ${status}`;
|
|
statusIndicator.title = `Connection status: ${status}`;
|
|
}
|
|
|
|
// Dispatch connection status event
|
|
window.dispatchEvent(new CustomEvent('connectionStatusChanged', {
|
|
detail: { status, isConnected: this.isConnected }
|
|
}));
|
|
}
|
|
|
|
handleScriptEvent(eventType, data) {
|
|
console.log(`Script ${eventType}:`, data);
|
|
|
|
// Update script status in UI
|
|
const scriptElements = document.querySelectorAll(`[data-script-id="${data.script_id}"]`);
|
|
scriptElements.forEach(element => {
|
|
const statusElement = element.querySelector('.script-status');
|
|
if (statusElement) {
|
|
statusElement.textContent = eventType;
|
|
statusElement.className = `script-status status-${eventType}`;
|
|
}
|
|
});
|
|
|
|
// Show notification
|
|
if (eventType === 'completed') {
|
|
this.showNotification(`Script completed successfully`, 'success');
|
|
} else if (eventType === 'failed') {
|
|
this.showNotification(`Script execution failed`, 'error');
|
|
}
|
|
|
|
// Call registered handlers
|
|
const handlers = this.messageHandlers.get('script_event') || [];
|
|
handlers.forEach(handler => handler(eventType, data));
|
|
}
|
|
|
|
handleScriptOutput(data) {
|
|
// Update log displays
|
|
const logContainers = document.querySelectorAll('.log-container[data-script-id=\"${data.script_id}\"]');
|
|
logContainers.forEach(container => {
|
|
this.appendLogEntry(container, data);
|
|
});
|
|
|
|
// Call registered handlers
|
|
const handlers = this.messageHandlers.get('script_output') || [];
|
|
handlers.forEach(handler => handler(data));
|
|
}
|
|
|
|
appendLogEntry(container, data) {
|
|
const logEntry = document.createElement('div');
|
|
logEntry.className = `log-entry log-${data.level || 'info'}`;
|
|
|
|
const timestamp = new Date(data.timestamp).toLocaleTimeString();
|
|
logEntry.innerHTML = `
|
|
<span class="log-timestamp">[${timestamp}]</span>
|
|
<span class="log-content">${this.escapeHtml(data.message)}</span>
|
|
`;
|
|
|
|
container.appendChild(logEntry);
|
|
|
|
// Auto-scroll to bottom
|
|
container.scrollTop = container.scrollHeight;
|
|
|
|
// Limit log entries to prevent memory issues
|
|
const maxEntries = 1000;
|
|
while (container.children.length > maxEntries) {
|
|
container.removeChild(container.firstChild);
|
|
}
|
|
}
|
|
|
|
handleSystemNotification(data) {
|
|
this.showNotification(data.message, data.type || 'info');
|
|
|
|
// Call registered handlers
|
|
const handlers = this.messageHandlers.get('system_notification') || [];
|
|
handlers.forEach(handler => handler(data));
|
|
}
|
|
|
|
handleCustomMessage(data) {
|
|
// Call registered handlers
|
|
const handlers = this.messageHandlers.get(data.type) || [];
|
|
handlers.forEach(handler => handler(data));
|
|
}
|
|
|
|
showNotification(message, type = 'info') {
|
|
if (typeof ScriptsManager !== 'undefined' && ScriptsManager.showNotification) {
|
|
ScriptsManager.showNotification(message, type);
|
|
} else {
|
|
console.log(`${type.toUpperCase()}: ${message}`);
|
|
}
|
|
}
|
|
|
|
escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
setupEventListeners() {
|
|
// Listen for page visibility changes to manage connection
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.hidden) {
|
|
// Page is hidden, reduce activity
|
|
this.emit('page_hidden');
|
|
} else {
|
|
// Page is visible, resume normal activity
|
|
this.emit('page_visible');
|
|
}
|
|
});
|
|
|
|
// Handle page unload
|
|
window.addEventListener('beforeunload', () => {
|
|
if (this.socket) {
|
|
this.socket.disconnect();
|
|
}
|
|
});
|
|
}
|
|
|
|
emitPendingEvents() {
|
|
// Emit any events that were queued while disconnected
|
|
// This could be implemented to store events in localStorage
|
|
// and replay them when reconnected
|
|
}
|
|
|
|
// Public API
|
|
on(eventType, handler) {
|
|
if (!this.messageHandlers.has(eventType)) {
|
|
this.messageHandlers.set(eventType, []);
|
|
}
|
|
this.messageHandlers.get(eventType).push(handler);
|
|
}
|
|
|
|
off(eventType, handler) {
|
|
const handlers = this.messageHandlers.get(eventType);
|
|
if (handlers) {
|
|
const index = handlers.indexOf(handler);
|
|
if (index > -1) {
|
|
handlers.splice(index, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
emit(eventType, data = {}) {
|
|
if (this.socket && this.isConnected) {
|
|
this.socket.emit(eventType, data);
|
|
} else {
|
|
console.warn('Cannot emit event - WebSocket not connected');
|
|
}
|
|
}
|
|
|
|
joinRoom(room) {
|
|
this.emit('join_room', { room });
|
|
}
|
|
|
|
leaveRoom(room) {
|
|
this.emit('leave_room', { room });
|
|
}
|
|
|
|
subscribeToScript(scriptId) {
|
|
this.emit('subscribe_script', { script_id: scriptId });
|
|
}
|
|
|
|
unsubscribeFromScript(scriptId) {
|
|
this.emit('unsubscribe_script', { script_id: scriptId });
|
|
}
|
|
|
|
disconnect() {
|
|
if (this.socket) {
|
|
this.socket.disconnect();
|
|
}
|
|
}
|
|
|
|
isConnectedToSocket() {
|
|
return this.isConnected;
|
|
}
|
|
}
|
|
|
|
// Log display component
|
|
class LogDisplay {
|
|
constructor(containerId, scriptId = null) {
|
|
this.container = document.getElementById(containerId);
|
|
this.scriptId = scriptId;
|
|
this.maxLines = 1000;
|
|
this.autoScroll = true;
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
if (!this.container) {
|
|
console.error(`Log container not found: ${containerId}`);
|
|
return;
|
|
}
|
|
|
|
this.container.classList.add('log-container');
|
|
if (this.scriptId) {
|
|
this.container.dataset.scriptId = this.scriptId;
|
|
}
|
|
|
|
this.setupControls();
|
|
this.setupEventListeners();
|
|
}
|
|
|
|
setupControls() {
|
|
const controls = document.createElement('div');
|
|
controls.className = 'log-controls mb-2';
|
|
controls.innerHTML = `
|
|
<div class="btn-group btn-group-sm" role="group">
|
|
<button type="button" class="btn btn-outline-secondary" id="log-clear">
|
|
<i class="bi bi-trash"></i> Clear
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary" id="log-scroll-toggle">
|
|
<i class="bi bi-arrow-down"></i> Auto-scroll
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary" id="log-download">
|
|
<i class="bi bi-download"></i> Download
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
this.container.parentNode.insertBefore(controls, this.container);
|
|
|
|
// Setup control event listeners
|
|
controls.querySelector('#log-clear').addEventListener('click', () => this.clear());
|
|
controls.querySelector('#log-scroll-toggle').addEventListener('click', () => this.toggleAutoScroll());
|
|
controls.querySelector('#log-download').addEventListener('click', () => this.downloadLogs());
|
|
}
|
|
|
|
setupEventListeners() {
|
|
// Listen for scroll events to disable auto-scroll when user scrolls up
|
|
this.container.addEventListener('scroll', () => {
|
|
const isScrolledToBottom = this.container.scrollHeight - this.container.clientHeight <= this.container.scrollTop + 1;
|
|
if (!isScrolledToBottom && this.autoScroll) {
|
|
this.autoScroll = false;
|
|
this.updateScrollToggle();
|
|
}
|
|
});
|
|
}
|
|
|
|
appendLog(message, level = 'info', timestamp = null) {
|
|
const logEntry = document.createElement('div');
|
|
logEntry.className = `log-entry log-${level}`;
|
|
|
|
const time = timestamp ? new Date(timestamp) : new Date();
|
|
const timeString = time.toLocaleTimeString();
|
|
|
|
logEntry.innerHTML = `
|
|
<span class="log-timestamp">[${timeString}]</span>
|
|
<span class="log-content">${this.escapeHtml(message)}</span>
|
|
`;
|
|
|
|
this.container.appendChild(logEntry);
|
|
|
|
// Limit number of entries
|
|
while (this.container.children.length > this.maxLines) {
|
|
this.container.removeChild(this.container.firstChild);
|
|
}
|
|
|
|
// Auto-scroll if enabled
|
|
if (this.autoScroll) {
|
|
this.scrollToBottom();
|
|
}
|
|
}
|
|
|
|
clear() {
|
|
this.container.innerHTML = '';
|
|
}
|
|
|
|
scrollToBottom() {
|
|
this.container.scrollTop = this.container.scrollHeight;
|
|
}
|
|
|
|
toggleAutoScroll() {
|
|
this.autoScroll = !this.autoScroll;
|
|
this.updateScrollToggle();
|
|
|
|
if (this.autoScroll) {
|
|
this.scrollToBottom();
|
|
}
|
|
}
|
|
|
|
updateScrollToggle() {
|
|
const toggle = document.getElementById('log-scroll-toggle');
|
|
if (toggle) {
|
|
const icon = toggle.querySelector('i');
|
|
if (this.autoScroll) {
|
|
icon.className = 'bi bi-arrow-down';
|
|
toggle.classList.add('active');
|
|
} else {
|
|
icon.className = 'bi bi-pause';
|
|
toggle.classList.remove('active');
|
|
}
|
|
}
|
|
}
|
|
|
|
downloadLogs() {
|
|
const logs = Array.from(this.container.children).map(entry => {
|
|
const timestamp = entry.querySelector('.log-timestamp').textContent;
|
|
const content = entry.querySelector('.log-content').textContent;
|
|
return `${timestamp} ${content}`;
|
|
}).join('\n');
|
|
|
|
const blob = new Blob([logs], { type: 'text/plain' });
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `logs-${new Date().toISOString().slice(0, 19)}.txt`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
}
|
|
|
|
// Initialize WebSocket when DOM is loaded
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Only initialize if user is authenticated
|
|
if (document.body.dataset.authenticated === 'true') {
|
|
window.webSocketManager = new WebSocketManager();
|
|
|
|
// Create global log display if container exists
|
|
const logContainer = document.getElementById('global-logs');
|
|
if (logContainer) {
|
|
window.globalLogDisplay = new LogDisplay('global-logs');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Export for use in other modules
|
|
window.WebSocketManager = WebSocketManager;
|
|
window.LogDisplay = LogDisplay; |