✨ 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!
429 lines
14 KiB
HTML
429 lines
14 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>🔌 WebSocket Test - CRM File Events</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh;
|
||
padding: 20px;
|
||
}
|
||
.container {
|
||
max-width: 900px;
|
||
margin: 0 auto;
|
||
background: white;
|
||
border-radius: 20px;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||
overflow: hidden;
|
||
}
|
||
.header {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
padding: 30px;
|
||
text-align: center;
|
||
}
|
||
.header h1 {
|
||
font-size: 2em;
|
||
margin-bottom: 10px;
|
||
}
|
||
.status {
|
||
padding: 20px;
|
||
background: #f8f9fa;
|
||
border-bottom: 2px solid #e9ecef;
|
||
}
|
||
.status-indicator {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 10px 20px;
|
||
border-radius: 50px;
|
||
font-weight: 600;
|
||
font-size: 1.1em;
|
||
}
|
||
.status-indicator.connected {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
}
|
||
.status-indicator.disconnected {
|
||
background: #f8d7da;
|
||
color: #721c24;
|
||
}
|
||
.status-indicator.connecting {
|
||
background: #fff3cd;
|
||
color: #856404;
|
||
}
|
||
.status-indicator .dot {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
animation: pulse 2s infinite;
|
||
}
|
||
.status-indicator.connected .dot {
|
||
background: #28a745;
|
||
}
|
||
.status-indicator.disconnected .dot {
|
||
background: #dc3545;
|
||
}
|
||
.status-indicator.connecting .dot {
|
||
background: #ffc107;
|
||
}
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.5; }
|
||
}
|
||
.stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 15px;
|
||
padding: 20px;
|
||
}
|
||
.stat-card {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
padding: 20px;
|
||
border-radius: 12px;
|
||
text-align: center;
|
||
}
|
||
.stat-value {
|
||
font-size: 2.5em;
|
||
font-weight: bold;
|
||
margin-bottom: 5px;
|
||
}
|
||
.stat-label {
|
||
font-size: 0.9em;
|
||
opacity: 0.9;
|
||
}
|
||
.controls {
|
||
padding: 20px;
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
background: #f8f9fa;
|
||
}
|
||
.btn {
|
||
padding: 12px 24px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 1em;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
}
|
||
.btn-primary {
|
||
background: #667eea;
|
||
color: white;
|
||
}
|
||
.btn-success {
|
||
background: #28a745;
|
||
color: white;
|
||
}
|
||
.btn-danger {
|
||
background: #dc3545;
|
||
color: white;
|
||
}
|
||
.btn-warning {
|
||
background: #ffc107;
|
||
color: #212529;
|
||
}
|
||
.events-container {
|
||
padding: 20px;
|
||
max-height: 500px;
|
||
overflow-y: auto;
|
||
}
|
||
.events-header {
|
||
font-size: 1.2em;
|
||
font-weight: 600;
|
||
margin-bottom: 15px;
|
||
color: #333;
|
||
}
|
||
.event-card {
|
||
background: #f8f9fa;
|
||
border-left: 4px solid #667eea;
|
||
padding: 15px;
|
||
margin-bottom: 10px;
|
||
border-radius: 8px;
|
||
animation: slideIn 0.3s ease-out;
|
||
}
|
||
@keyframes slideIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateX(-20px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateX(0);
|
||
}
|
||
}
|
||
.event-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
.event-type {
|
||
display: inline-block;
|
||
padding: 4px 12px;
|
||
border-radius: 20px;
|
||
font-size: 0.85em;
|
||
font-weight: 600;
|
||
background: #667eea;
|
||
color: white;
|
||
}
|
||
.event-time {
|
||
font-size: 0.85em;
|
||
color: #6c757d;
|
||
}
|
||
.event-data {
|
||
background: white;
|
||
padding: 10px;
|
||
border-radius: 6px;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 0.9em;
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
}
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 40px;
|
||
color: #6c757d;
|
||
}
|
||
.empty-state-icon {
|
||
font-size: 4em;
|
||
margin-bottom: 10px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>🔌 WebSocket Test</h1>
|
||
<p>CRM File Events - Real-time Updates</p>
|
||
</div>
|
||
|
||
<div class="status">
|
||
<div class="status-indicator disconnected" id="statusIndicator">
|
||
<span class="dot"></span>
|
||
<span id="statusText">Отключено</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stats">
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="eventCount">0</div>
|
||
<div class="stat-label">Всего событий</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="connectionTime">0s</div>
|
||
<div class="stat-label">Время подключения</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="reconnectCount">0</div>
|
||
<div class="stat-label">Переподключений</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="controls">
|
||
<button class="btn btn-primary" onclick="connectWebSocket()">🔄 Подключиться</button>
|
||
<button class="btn btn-danger" onclick="disconnectWebSocket()">🔌 Отключиться</button>
|
||
<button class="btn btn-success" onclick="sendTestEvent()">🧪 Тест события</button>
|
||
<button class="btn btn-warning" onclick="clearEvents()">🗑️ Очистить</button>
|
||
</div>
|
||
|
||
<div class="events-container">
|
||
<div class="events-header">📋 События:</div>
|
||
<div id="eventsLog">
|
||
<div class="empty-state">
|
||
<div class="empty-state-icon">📭</div>
|
||
<p>Нет событий. Подключитесь к WebSocket!</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let ws = null;
|
||
let eventCount = 0;
|
||
let reconnectCount = 0;
|
||
let connectionStartTime = null;
|
||
let connectionTimer = null;
|
||
|
||
// Автоподключение при загрузке
|
||
window.addEventListener('load', () => {
|
||
connectWebSocket();
|
||
});
|
||
|
||
function connectWebSocket() {
|
||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||
console.log('✅ Already connected');
|
||
return;
|
||
}
|
||
|
||
updateStatus('connecting', 'Подключение...');
|
||
|
||
// WebSocket URL
|
||
const wsUrl = 'wss://crm.clientright.ru/ws';
|
||
console.log('🔌 Connecting to:', wsUrl);
|
||
|
||
ws = new WebSocket(wsUrl);
|
||
|
||
ws.onopen = () => {
|
||
console.log('✅ WebSocket connected');
|
||
updateStatus('connected', 'Подключено');
|
||
connectionStartTime = Date.now();
|
||
startConnectionTimer();
|
||
reconnectCount++;
|
||
updateStats();
|
||
};
|
||
|
||
ws.onmessage = (event) => {
|
||
console.log('📨 Received:', event.data);
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
addEventToLog(data);
|
||
eventCount++;
|
||
updateStats();
|
||
} catch (e) {
|
||
console.error('❌ Parse error:', e);
|
||
addEventToLog({ raw: event.data });
|
||
}
|
||
};
|
||
|
||
ws.onerror = (error) => {
|
||
console.error('❌ WebSocket error:', error);
|
||
updateStatus('disconnected', 'Ошибка подключения');
|
||
};
|
||
|
||
ws.onclose = (event) => {
|
||
console.log('🔌 WebSocket closed:', event.code, event.reason);
|
||
updateStatus('disconnected', `Отключено (${event.code})`);
|
||
stopConnectionTimer();
|
||
|
||
// Автоматическое переподключение через 5 секунд
|
||
setTimeout(() => {
|
||
if (!ws || ws.readyState === WebSocket.CLOSED) {
|
||
console.log('🔄 Auto-reconnecting...');
|
||
connectWebSocket();
|
||
}
|
||
}, 5000);
|
||
};
|
||
}
|
||
|
||
function disconnectWebSocket() {
|
||
if (ws) {
|
||
ws.close(1000, 'User requested disconnect');
|
||
ws = null;
|
||
stopConnectionTimer();
|
||
}
|
||
}
|
||
|
||
function sendTestEvent() {
|
||
// Отправляем тестовое событие через Redis
|
||
fetch('/crm_extensions/file_storage/api/send_test_event.php', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
type: 'test',
|
||
data: {
|
||
message: 'Тестовое событие из браузера!',
|
||
timestamp: Date.now()
|
||
}
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(result => {
|
||
console.log('✅ Test event sent:', result);
|
||
})
|
||
.catch(error => {
|
||
console.error('❌ Failed to send test event:', error);
|
||
});
|
||
}
|
||
|
||
function updateStatus(status, text) {
|
||
const indicator = document.getElementById('statusIndicator');
|
||
const statusText = document.getElementById('statusText');
|
||
|
||
indicator.className = `status-indicator ${status}`;
|
||
statusText.textContent = text;
|
||
}
|
||
|
||
function addEventToLog(eventData) {
|
||
const eventsLog = document.getElementById('eventsLog');
|
||
|
||
// Удаляем пустое состояние
|
||
if (eventsLog.querySelector('.empty-state')) {
|
||
eventsLog.innerHTML = '';
|
||
}
|
||
|
||
const eventCard = document.createElement('div');
|
||
eventCard.className = 'event-card';
|
||
|
||
const now = new Date();
|
||
const timeString = now.toLocaleTimeString('ru-RU');
|
||
|
||
eventCard.innerHTML = `
|
||
<div class="event-header">
|
||
<span class="event-type">${eventData.type || 'unknown'}</span>
|
||
<span class="event-time">${timeString}</span>
|
||
</div>
|
||
<div class="event-data">${JSON.stringify(eventData, null, 2)}</div>
|
||
`;
|
||
|
||
eventsLog.insertBefore(eventCard, eventsLog.firstChild);
|
||
|
||
// Ограничиваем количество событий до 50
|
||
while (eventsLog.children.length > 50) {
|
||
eventsLog.removeChild(eventsLog.lastChild);
|
||
}
|
||
}
|
||
|
||
function clearEvents() {
|
||
eventCount = 0;
|
||
document.getElementById('eventsLog').innerHTML = `
|
||
<div class="empty-state">
|
||
<div class="empty-state-icon">📭</div>
|
||
<p>События очищены</p>
|
||
</div>
|
||
`;
|
||
updateStats();
|
||
}
|
||
|
||
function updateStats() {
|
||
document.getElementById('eventCount').textContent = eventCount;
|
||
document.getElementById('reconnectCount').textContent = reconnectCount;
|
||
}
|
||
|
||
function startConnectionTimer() {
|
||
stopConnectionTimer();
|
||
connectionTimer = setInterval(() => {
|
||
if (connectionStartTime) {
|
||
const elapsed = Math.floor((Date.now() - connectionStartTime) / 1000);
|
||
document.getElementById('connectionTime').textContent = `${elapsed}s`;
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
function stopConnectionTimer() {
|
||
if (connectionTimer) {
|
||
clearInterval(connectionTimer);
|
||
connectionTimer = null;
|
||
}
|
||
connectionStartTime = null;
|
||
document.getElementById('connectionTime').textContent = '0s';
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|