✨ Features: - Migrated ALL files to new S3 structure (Projects, Contacts, Accounts, HelpDesk, Invoice, etc.) - Added Nextcloud folder buttons to ALL modules - Fixed Nextcloud editor integration - WebSocket server for real-time updates - Redis Pub/Sub integration - File path manager for organized storage - Redis caching for performance (Functions.php) 📁 New Structure: Documents/Project/ProjectName_ID/file_docID.ext Documents/Contacts/FirstName_LastName_ID/file_docID.ext Documents/Accounts/AccountName_ID/file_docID.ext 🔧 Technical: - FilePathManager for standardized paths - S3StorageService integration - WebSocket server (Node.js + Docker) - Redis cache for getBasicModuleInfo() - Predis library for Redis connectivity 📝 Scripts: - Migration scripts for all modules - Test pages for WebSocket/SSE/Polling - Documentation (MIGRATION_*.md, REDIS_*.md) 🎯 Result: 15,000+ files migrated successfully!
295 lines
12 KiB
JavaScript
295 lines
12 KiB
JavaScript
/**
|
||
* SSE (Server-Sent Events) клиент для синхронизации файлов в реальном времени
|
||
*
|
||
* Автоматически подключается к SSE endpoint и обновляет UI при изменениях файлов
|
||
*/
|
||
|
||
class FileSyncSSE {
|
||
constructor() {
|
||
this.eventSource = null;
|
||
this.reconnectInterval = 5000; // 5 секунд
|
||
this.maxReconnectAttempts = 10;
|
||
this.reconnectAttempts = 0;
|
||
this.isConnected = false;
|
||
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
console.log('🔄 Инициализация SSE для синхронизации файлов...');
|
||
this.connect();
|
||
}
|
||
|
||
connect() {
|
||
try {
|
||
// Закрываем предыдущее соединение
|
||
if (this.eventSource) {
|
||
this.eventSource.close();
|
||
}
|
||
|
||
// Создаем новое SSE соединение
|
||
this.eventSource = new EventSource('/crm_extensions/file_storage/api/sse_events.php');
|
||
|
||
// Обработчик успешного подключения
|
||
this.eventSource.onopen = (event) => {
|
||
console.log('✅ SSE подключение установлено');
|
||
this.isConnected = true;
|
||
this.reconnectAttempts = 0;
|
||
this.showConnectionStatus('connected');
|
||
};
|
||
|
||
// Обработчик сообщений
|
||
this.eventSource.onmessage = (event) => {
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
this.handleEvent(data);
|
||
} catch (error) {
|
||
console.error('❌ Ошибка парсинга SSE данных:', error);
|
||
}
|
||
};
|
||
|
||
// Обработчик ошибок
|
||
this.eventSource.onerror = (event) => {
|
||
console.error('❌ SSE ошибка:', event);
|
||
this.isConnected = false;
|
||
this.showConnectionStatus('disconnected');
|
||
|
||
// Попытка переподключения
|
||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||
this.reconnectAttempts++;
|
||
console.log(`🔄 Попытка переподключения ${this.reconnectAttempts}/${this.maxReconnectAttempts}...`);
|
||
|
||
setTimeout(() => {
|
||
this.connect();
|
||
}, this.reconnectInterval);
|
||
} else {
|
||
console.error('❌ Максимальное количество попыток переподключения достигнуто');
|
||
this.showConnectionStatus('failed');
|
||
}
|
||
};
|
||
|
||
} catch (error) {
|
||
console.error('❌ Ошибка создания SSE соединения:', error);
|
||
this.showConnectionStatus('error');
|
||
}
|
||
}
|
||
|
||
handleEvent(data) {
|
||
console.log('📨 SSE событие:', data);
|
||
|
||
switch (data.type) {
|
||
case 'connected':
|
||
console.log('✅ SSE подключен:', data.data.message);
|
||
break;
|
||
|
||
case 'disconnected':
|
||
console.log('❌ SSE отключен:', data.data.message);
|
||
break;
|
||
|
||
case 'heartbeat':
|
||
// Heartbeat - просто обновляем статус
|
||
break;
|
||
|
||
case 'file_created':
|
||
this.handleFileCreated(data.data);
|
||
break;
|
||
|
||
case 'file_updated':
|
||
this.handleFileUpdated(data.data);
|
||
break;
|
||
|
||
case 'file_deleted':
|
||
this.handleFileDeleted(data.data);
|
||
break;
|
||
|
||
case 'folder_renamed':
|
||
this.handleFolderRenamed(data.data);
|
||
break;
|
||
|
||
case 'folder_deleted':
|
||
this.handleFolderDeleted(data.data);
|
||
break;
|
||
|
||
default:
|
||
console.log('❓ Неизвестное SSE событие:', data.type);
|
||
}
|
||
}
|
||
|
||
handleFileCreated(data) {
|
||
console.log('📄 Файл создан:', data);
|
||
|
||
// Показываем уведомление
|
||
this.showNotification('Файл добавлен', `Файл "${data.fileName}" добавлен в ${data.module}`, 'success');
|
||
|
||
// Обновляем список файлов если мы на странице детального просмотра
|
||
this.refreshFileList(data.module, data.recordId);
|
||
}
|
||
|
||
handleFileUpdated(data) {
|
||
console.log('📝 Файл обновлен:', data);
|
||
|
||
// Показываем уведомление
|
||
this.showNotification('Файл обновлен', `Файл "${data.fileName}" обновлен в ${data.module}`, 'info');
|
||
|
||
// Обновляем список файлов
|
||
this.refreshFileList(data.module, data.recordId);
|
||
}
|
||
|
||
handleFileDeleted(data) {
|
||
console.log('🗑️ Файл удален:', data);
|
||
|
||
// Показываем уведомление
|
||
this.showNotification('Файл удален', `Файл "${data.fileName}" удален из ${data.module}`, 'warning');
|
||
|
||
// Обновляем список файлов
|
||
this.refreshFileList(data.module, data.recordId);
|
||
}
|
||
|
||
handleFolderRenamed(data) {
|
||
console.log('📁 Папка переименована:', data);
|
||
|
||
// Показываем уведомление
|
||
this.showNotification('Папка переименована', `Папка переименована в ${data.module}`, 'info');
|
||
|
||
// Обновляем список файлов
|
||
this.refreshFileList(data.module, data.recordId);
|
||
}
|
||
|
||
handleFolderDeleted(data) {
|
||
console.log('🗂️ Папка удалена:', data);
|
||
|
||
// Показываем уведомление
|
||
this.showNotification('Папка удалена', `Папка удалена из ${data.module}`, 'error');
|
||
|
||
// Обновляем список файлов
|
||
this.refreshFileList(data.module, data.recordId);
|
||
}
|
||
|
||
refreshFileList(module, recordId) {
|
||
// Проверяем, находимся ли мы на странице детального просмотра нужного модуля
|
||
const currentModule = window.location.search.match(/module=([^&]+)/);
|
||
const currentRecord = window.location.search.match(/record=([^&]+)/);
|
||
|
||
if (currentModule && currentModule[1] === module &&
|
||
currentRecord && currentRecord[1] === recordId) {
|
||
|
||
console.log('🔄 Обновляем список файлов...');
|
||
|
||
// Обновляем страницу или конкретный блок с файлами
|
||
if (typeof refreshFileList === 'function') {
|
||
refreshFileList();
|
||
} else {
|
||
// Fallback - обновляем всю страницу
|
||
setTimeout(() => {
|
||
window.location.reload();
|
||
}, 1000);
|
||
}
|
||
}
|
||
}
|
||
|
||
showNotification(title, message, type = 'info') {
|
||
// Используем существующую систему уведомлений CRM
|
||
if (typeof Vtiger_Helper_Js !== 'undefined' && Vtiger_Helper_Js.showPnotify) {
|
||
Vtiger_Helper_Js.showPnotify({
|
||
title: title,
|
||
text: message,
|
||
type: type,
|
||
delay: 5000
|
||
});
|
||
} else {
|
||
// Fallback - обычный alert
|
||
alert(`${title}: ${message}`);
|
||
}
|
||
}
|
||
|
||
showConnectionStatus(status) {
|
||
// Создаем или обновляем индикатор статуса подключения
|
||
let statusElement = document.getElementById('sse-connection-status');
|
||
|
||
if (!statusElement) {
|
||
statusElement = document.createElement('div');
|
||
statusElement.id = 'sse-connection-status';
|
||
statusElement.style.cssText = `
|
||
position: fixed;
|
||
top: 10px;
|
||
right: 10px;
|
||
padding: 8px 12px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
z-index: 9999;
|
||
transition: all 0.3s ease;
|
||
`;
|
||
document.body.appendChild(statusElement);
|
||
}
|
||
|
||
switch (status) {
|
||
case 'connected':
|
||
statusElement.textContent = '🟢 Файлы синхронизируются';
|
||
statusElement.style.backgroundColor = '#d4edda';
|
||
statusElement.style.color = '#155724';
|
||
statusElement.style.border = '1px solid #c3e6cb';
|
||
break;
|
||
|
||
case 'disconnected':
|
||
statusElement.textContent = '🟡 Переподключение...';
|
||
statusElement.style.backgroundColor = '#fff3cd';
|
||
statusElement.style.color = '#856404';
|
||
statusElement.style.border = '1px solid #ffeaa7';
|
||
break;
|
||
|
||
case 'failed':
|
||
statusElement.textContent = '🔴 Синхронизация недоступна';
|
||
statusElement.style.backgroundColor = '#f8d7da';
|
||
statusElement.style.color = '#721c24';
|
||
statusElement.style.border = '1px solid #f5c6cb';
|
||
break;
|
||
|
||
case 'error':
|
||
statusElement.textContent = '❌ Ошибка подключения';
|
||
statusElement.style.backgroundColor = '#f8d7da';
|
||
statusElement.style.color = '#721c24';
|
||
statusElement.style.border = '1px solid #f5c6cb';
|
||
break;
|
||
}
|
||
|
||
// Автоматически скрываем через 5 секунд для успешного подключения
|
||
if (status === 'connected') {
|
||
setTimeout(() => {
|
||
if (statusElement) {
|
||
statusElement.style.opacity = '0.7';
|
||
}
|
||
}, 5000);
|
||
}
|
||
}
|
||
|
||
disconnect() {
|
||
if (this.eventSource) {
|
||
this.eventSource.close();
|
||
this.eventSource = null;
|
||
}
|
||
this.isConnected = false;
|
||
console.log('🔌 SSE соединение закрыто');
|
||
}
|
||
}
|
||
|
||
// Автоматически инициализируем SSE при загрузке страницы
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// Проверяем, что мы в CRM (не в админке или других разделах)
|
||
if (window.location.pathname.includes('/index.php') &&
|
||
!window.location.pathname.includes('/admin') &&
|
||
!window.location.pathname.includes('/install')) {
|
||
|
||
console.log('🚀 Запуск SSE синхронизации файлов...');
|
||
window.fileSyncSSE = new FileSyncSSE();
|
||
}
|
||
});
|
||
|
||
// Экспортируем для использования в других модулях
|
||
if (typeof module !== 'undefined' && module.exports) {
|
||
module.exports = FileSyncSSE;
|
||
}
|
||
|
||
|
||
|
||
|