SIDEL_ScriptsManager/app/static/js/websocket.js

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;