Files
crm.clientright.ru/crm_extensions/file_storage/test_websocket.html
Fedor 9245768987 🚀 CRM Files Migration & Real-time Features
 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!
2025-10-24 19:59:28 +03:00

429 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>