// Глобальные переменные let socket = null; let androidSocket = null; let operatorSocket = null; let localStream = null; let peerConnection = null; let currentSessionId = null; // Конфигурация WebRTC const rtcConfig = { iceServers: [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' } ] }; // Инициализация при загрузке страницы document.addEventListener('DOMContentLoaded', function() { initializeSocket(); loadSystemStats(); // Обновление статистики каждые 5 секунд setInterval(loadSystemStats, 5000); }); // Управление вкладками function showTab(tabName) { // Скрываем все вкладки document.querySelectorAll('.tab-content').forEach(tab => { tab.classList.remove('active'); }); // Убираем активный класс с кнопок document.querySelectorAll('.tab').forEach(btn => { btn.classList.remove('active'); }); // Показываем выбранную вкладку document.getElementById(tabName).classList.add('active'); event.target.classList.add('active'); } // Инициализация основного сокета для мониторинга function initializeSocket() { socket = io(); socket.on('connect', () => { logMessage('info', 'Подключение к серверу установлено'); updateConnectionStatus(true); }); socket.on('disconnect', () => { logMessage('warn', 'Подключение к серверу потеряно'); updateConnectionStatus(false); }); socket.on('device:connected', (data) => { logMessage('info', `Устройство подключено: ${data.deviceId}`); }); socket.on('device:disconnected', (data) => { logMessage('warn', `Устройство отключено: ${data.deviceId}`); }); } // Обновление статуса подключения function updateConnectionStatus(connected) { const statusCard = document.getElementById('connection-status'); const statusText = document.getElementById('connection-text'); if (connected) { statusCard.className = 'status-card status-connected'; statusText.textContent = '✅ Подключено к серверу'; } else { statusCard.className = 'status-card status-disconnected'; statusText.textContent = '❌ Нет подключения к серверу'; } } // Загрузка системной статистики async function loadSystemStats() { try { const response = await fetch('/api/status'); const data = await response.json(); if (data.devices && data.sessions) { document.getElementById('stat-devices').textContent = data.devices.connectedDevices || 0; document.getElementById('stat-operators').textContent = data.devices.connectedOperators || 0; document.getElementById('stat-sessions').textContent = data.sessions.activeSessions || 0; document.getElementById('stat-uptime').textContent = Math.round(data.uptime / 60) || 0; } } catch (error) { console.error('Ошибка загрузки статистики:', error); } } // Система логирования function logMessage(level, message) { const logs = document.getElementById('system-logs'); const timestamp = new Date().toLocaleTimeString(); const logEntry = document.createElement('div'); logEntry.className = `log-entry log-${level}`; logEntry.textContent = `[${timestamp}] ${message}`; logs.appendChild(logEntry); logs.scrollTop = logs.scrollHeight; // Ограничиваем количество логов const maxLogs = 100; while (logs.children.length > maxLogs) { logs.removeChild(logs.firstChild); } } function clearLogs() { document.getElementById('system-logs').innerHTML = ''; } // === ANDROID DEVICE SIMULATION === function connectAndroid() { const deviceId = document.getElementById('android-device-id').value; if (!deviceId) { alert('Введите Device ID'); return; } androidSocket = io(); androidSocket.on('connect', () => { const deviceInfo = { model: document.getElementById('android-model').value, manufacturer: document.getElementById('android-manufacturer').value, androidVersion: document.getElementById('android-version').value, availableCameras: Array.from(document.getElementById('android-cameras').selectedOptions) .map(option => option.value).join(',') }; androidSocket.emit('register:android', { deviceId, deviceInfo }); logMessage('info', `Android устройство подключается: ${deviceId}`); }); androidSocket.on('register:success', (data) => { logMessage('info', `Android устройство зарегистрировано: ${data.deviceId}`); document.getElementById('android-connect').disabled = true; document.getElementById('android-disconnect').disabled = false; }); androidSocket.on('camera:request', (data) => { console.log('🎥 ПОЛУЧЕН ЗАПРОС КАМЕРЫ:', data); logMessage('info', `🎥 Получен запрос камеры: ${JSON.stringify(data)}`); showCameraRequest(data); }); androidSocket.on('camera:switch', (data) => { logMessage('info', `Запрос переключения камеры: ${data.cameraType}`); showAlert('info', `Переключение камеры на: ${data.cameraType}`); }); androidSocket.on('camera:disconnect', (data) => { logMessage('warn', `Сессия завершена: ${data.sessionId}`); removeCameraSession(data.sessionId); }); setupAndroidWebRTC(); } function disconnectAndroid() { if (androidSocket) { androidSocket.disconnect(); androidSocket = null; logMessage('warn', 'Android устройство отключено'); document.getElementById('android-connect').disabled = false; document.getElementById('android-disconnect').disabled = true; // Очищаем запросы и сессии document.getElementById('android-requests').innerHTML = ''; document.getElementById('android-sessions').innerHTML = ''; } } function showCameraRequest(data) { console.log('📱 ПОКАЗЫВАЕМ ЗАПРОС КАМЕРЫ:', data); const container = document.getElementById('android-requests'); const requestDiv = document.createElement('div'); requestDiv.className = 'session-card session-pending'; requestDiv.innerHTML = `

Запрос камеры

Сессия: ${data.sessionId}

Оператор: ${data.operatorId}

Камера: ${data.cameraType}

`; container.appendChild(requestDiv); logMessage('info', `Получен запрос камеры от ${data.operatorId}`); // Автоматически принимаем запрос через 1 секунду для удобства тестирования console.log('⏰ ЗАПЛАНИРОВАНО АВТОПРИНЯТИЕ ЧЕРЕЗ 1 СЕКУНДУ'); setTimeout(() => { console.log('⏰ ВЫПОЛНЯЕМ АВТОПРИНЯТИЕ'); acceptCameraRequest(data.sessionId); }, 1000); } function acceptCameraRequest(sessionId) { console.log('✅ ПРИНЯТИЕ ЗАПРОСА КАМЕРЫ:', sessionId); if (androidSocket) { androidSocket.emit('camera:response', { sessionId, accepted: true, streamUrl: 'webrtc' }); console.log('✅ ОТПРАВЛЕНО camera:response с accepted=true'); // Перемещаем в активные сессии moveToActiveSessions(sessionId, 'active'); logMessage('info', `Запрос камеры принят: ${sessionId}`); } else { console.error('❌ androidSocket НЕ ПОДКЛЮЧЕН!'); } } function declineCameraRequest(sessionId) { if (androidSocket) { androidSocket.emit('camera:response', { sessionId, accepted: false, error: 'Пользователь отклонил запрос' }); // Удаляем запрос removeCameraRequest(sessionId); logMessage('warn', `Запрос камеры отклонен: ${sessionId}`); } } function moveToActiveSessions(sessionId, status) { removeCameraRequest(sessionId); const container = document.getElementById('android-sessions'); const sessionDiv = document.createElement('div'); sessionDiv.className = `session-card session-${status}`; sessionDiv.id = `android-session-${sessionId}`; sessionDiv.innerHTML = `

Активная сессия

ID: ${sessionId}

Статус: ${status}

`; container.appendChild(sessionDiv); currentSessionId = sessionId; document.getElementById('current-session-id').value = sessionId; } function removeCameraRequest(sessionId) { const container = document.getElementById('android-requests'); const requests = container.children; for (let i = 0; i < requests.length; i++) { if (requests[i].innerHTML.includes(sessionId)) { container.removeChild(requests[i]); break; } } } function removeCameraSession(sessionId) { const sessionElement = document.getElementById(`android-session-${sessionId}`); if (sessionElement) { sessionElement.remove(); } if (currentSessionId === sessionId) { currentSessionId = null; document.getElementById('current-session-id').value = ''; } } function endAndroidSession(sessionId) { if (androidSocket) { androidSocket.emit('camera:disconnect', { sessionId }); removeCameraSession(sessionId); } } // === OPERATOR SIMULATION === function connectOperator() { const operatorId = document.getElementById('operator-id').value; if (!operatorId) { alert('Введите Operator ID'); return; } operatorSocket = io(); operatorSocket.on('connect', () => { operatorSocket.emit('register:operator', { operatorId, operatorInfo: { name: 'Demo Operator', permissions: ['view_cameras', 'request_camera'] } }); logMessage('info', `Оператор подключается: ${operatorId}`); }); operatorSocket.on('register:success', (data) => { logMessage('info', `Оператор зарегистрирован: ${data.operatorId}`); document.getElementById('operator-connect').disabled = true; document.getElementById('operator-disconnect').disabled = false; showAvailableDevices(data.availableDevices || []); }); operatorSocket.on('device:connected', (data) => { logMessage('info', `Новое устройство доступно: ${data.deviceId}`); refreshDevices(); }); operatorSocket.on('camera:stream-ready', (data) => { logMessage('info', `Камера готова: ${data.sessionId}`); showOperatorSession(data.sessionId, 'active'); }); operatorSocket.on('camera:denied', (data) => { logMessage('warn', `Запрос отклонен: ${data.error}`); showAlert('warning', `Запрос камеры отклонен: ${data.error}`); }); setupOperatorWebRTC(); } function disconnectOperator() { if (operatorSocket) { operatorSocket.disconnect(); operatorSocket = null; logMessage('warn', 'Оператор отключен'); document.getElementById('operator-connect').disabled = false; document.getElementById('operator-disconnect').disabled = true; // Очищаем списки document.getElementById('available-devices').innerHTML = ''; document.getElementById('operator-sessions').innerHTML = ''; } } async function refreshDevices() { const operatorId = document.getElementById('operator-id').value; if (!operatorId) return; try { const response = await fetch('/api/operators/devices', { headers: { 'X-Operator-Id': operatorId } }); if (response.ok) { const data = await response.json(); showAvailableDevices(data.devices || []); } } catch (error) { logMessage('error', 'Ошибка загрузки устройств: ' + error.message); } } function showAvailableDevices(devices) { const container = document.getElementById('available-devices'); container.innerHTML = ''; devices.forEach(device => { const deviceDiv = document.createElement('div'); deviceDiv.className = `device-card ${device.isConnected ? 'device-online' : 'device-offline'}`; deviceDiv.innerHTML = `

${device.deviceInfo.model || 'Unknown Device'}

ID: ${device.deviceId}

Статус: ${device.status}

Камеры: ${device.capabilities?.cameras?.join(', ') || 'Unknown'}

${device.canAcceptSession ? ` ` : '

Недоступен для новых сессий

' } `; container.appendChild(deviceDiv); }); } function getCameraName(cameraType) { const names = { 'back': 'Основная', 'front': 'Фронтальная', 'ultra_wide': 'Широкоугольная', 'telephoto': 'Телеобъектив' }; return names[cameraType] || cameraType; } async function requestCamera(deviceId) { const operatorId = document.getElementById('operator-id').value; const cameraSelect = document.getElementById(`camera-${deviceId}`); const cameraType = cameraSelect ? cameraSelect.value : 'back'; try { const response = await fetch('/api/operators/camera/request', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Operator-Id': operatorId }, body: JSON.stringify({ deviceId, cameraType }) }); if (response.ok) { const data = await response.json(); logMessage('info', `Запрос камеры отправлен: ${data.sessionId}`); showOperatorSession(data.sessionId, 'pending'); } else { const error = await response.json(); showAlert('danger', 'Ошибка запроса камеры: ' + error.error); } } catch (error) { logMessage('error', 'Ошибка запроса камеры: ' + error.message); } } function showOperatorSession(sessionId, status) { const container = document.getElementById('operator-sessions'); const existingSession = document.getElementById(`operator-session-${sessionId}`); if (existingSession) { // Обновляем существующую сессию existingSession.className = `session-card session-${status}`; const statusElement = existingSession.querySelector('.session-status'); if (statusElement) statusElement.textContent = status; return; } const sessionDiv = document.createElement('div'); sessionDiv.className = `session-card session-${status}`; sessionDiv.id = `operator-session-${sessionId}`; sessionDiv.innerHTML = `

Сессия камеры

ID: ${sessionId}

Статус: ${status}

`; container.appendChild(sessionDiv); currentSessionId = sessionId; document.getElementById('current-session-id').value = sessionId; } async function switchCamera(sessionId, cameraType) { const operatorId = document.getElementById('operator-id').value; try { const response = await fetch(`/api/operators/camera/${sessionId}/switch`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Operator-Id': operatorId }, body: JSON.stringify({ cameraType }) }); if (response.ok) { logMessage('info', `Переключение камеры: ${cameraType}`); showAlert('info', `Запрос переключения камеры на: ${getCameraName(cameraType)}`); } else { const error = await response.json(); showAlert('danger', 'Ошибка переключения камеры: ' + error.error); } } catch (error) { logMessage('error', 'Ошибка переключения камеры: ' + error.message); } } async function endOperatorSession(sessionId) { const operatorId = document.getElementById('operator-id').value; try { const response = await fetch(`/api/operators/camera/${sessionId}`, { method: 'DELETE', headers: { 'X-Operator-Id': operatorId } }); if (response.ok) { const sessionElement = document.getElementById(`operator-session-${sessionId}`); if (sessionElement) sessionElement.remove(); if (currentSessionId === sessionId) { currentSessionId = null; document.getElementById('current-session-id').value = ''; } logMessage('info', `Сессия завершена: ${sessionId}`); } } catch (error) { logMessage('error', 'Ошибка завершения сессии: ' + error.message); } } async function refreshSessions() { const operatorId = document.getElementById('operator-id').value; if (!operatorId) return; try { const response = await fetch('/api/operators/sessions', { headers: { 'X-Operator-Id': operatorId } }); if (response.ok) { const data = await response.json(); const container = document.getElementById('operator-sessions'); container.innerHTML = ''; data.sessions.forEach(session => { showOperatorSession(session.sessionId, session.status); }); } } catch (error) { logMessage('error', 'Ошибка загрузки сессий: ' + error.message); } } // === ADMIN FUNCTIONS === async function getSystemHealth() { try { const response = await fetch('/api/admin/health'); const data = await response.json(); const container = document.getElementById('admin-info'); container.innerHTML = `

Состояние системы: ${data.health.status}

Память: ${data.health.memory.used}MB / ${data.health.memory.total}MB

Время работы: ${data.health.uptime} сек

Подключения: ${data.health.connections.devices} устройств, ${data.health.connections.operators} операторов

${data.health.warnings ? `

Предупреждения: ${data.health.warnings.join(', ')}

` : ''}
`; } catch (error) { showAlert('danger', 'Ошибка проверки здоровья: ' + error.message); } } async function getSystemStats() { try { const response = await fetch('/api/admin/stats'); const data = await response.json(); const container = document.getElementById('admin-info'); container.innerHTML = `

Детальная статистика

${JSON.stringify(data.stats, null, 2)}
`; } catch (error) { showAlert('danger', 'Ошибка загрузки статистики: ' + error.message); } } async function cleanupSystem() { try { const response = await fetch('/api/admin/cleanup', { method: 'POST' }); const data = await response.json(); showAlert('success', `Очистка завершена. Удалено сессий: ${data.removedSessions}`); logMessage('info', `Системная очистка: удалено ${data.removedSessions} сессий`); } catch (error) { showAlert('danger', 'Ошибка очистки: ' + error.message); } } async function getAllDevices() { try { const response = await fetch('/api/admin/devices'); const data = await response.json(); const container = document.getElementById('all-devices'); container.innerHTML = ''; data.devices.forEach(device => { const deviceDiv = document.createElement('div'); deviceDiv.className = `device-card ${device.isConnected ? 'device-online' : 'device-offline'}`; deviceDiv.innerHTML = `
${device.deviceInfo.model || 'Unknown Device'}

ID: ${device.deviceId}

Статус: ${device.status}

Активных сессий: ${device.activeSessions}

Время работы: ${device.uptime} сек

`; container.appendChild(deviceDiv); }); } catch (error) { showAlert('danger', 'Ошибка загрузки устройств: ' + error.message); } } async function getAllSessions() { try { const response = await fetch('/api/admin/sessions'); const data = await response.json(); const container = document.getElementById('all-sessions'); container.innerHTML = ''; data.sessions.forEach(session => { const sessionDiv = document.createElement('div'); sessionDiv.className = `session-card session-${session.status}`; sessionDiv.innerHTML = `
Сессия ${session.sessionId}

Устройство: ${session.deviceId}

Оператор: ${session.operatorId}

Статус: ${session.status}

Продолжительность: ${session.duration}

Камера: ${getCameraName(session.cameraType)}

`; container.appendChild(sessionDiv); }); } catch (error) { showAlert('danger', 'Ошибка загрузки сессий: ' + error.message); } } // === WebRTC TEST === function setupAndroidWebRTC() { if (!androidSocket) return; androidSocket.on('webrtc:offer', async (data) => { try { const pc = getOrCreatePeerConnection(); await pc.setRemoteDescription(new RTCSessionDescription({ type: 'offer', sdp: data.offer })); const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); androidSocket.emit('webrtc:answer', { sessionId: data.sessionId, answer: answer.sdp }); logMessage('info', 'WebRTC answer отправлен'); } catch (error) { logMessage('error', 'WebRTC ошибка: ' + error.message); } }); androidSocket.on('webrtc:ice-candidate', async (data) => { try { logMessage('info', `📡 Получен ICE candidate: ${JSON.stringify(data.candidate)}`); const pc = getOrCreatePeerConnection(); const candidate = data.candidate; await pc.addIceCandidate(new RTCIceCandidate(candidate)); logMessage('success', '✅ ICE candidate добавлен успешно'); } catch (error) { logMessage('error', 'ICE candidate ошибка: ' + error.message); } }); } function setupOperatorWebRTC() { if (!operatorSocket) return; operatorSocket.on('webrtc:answer', async (data) => { try { const pc = getOrCreatePeerConnection(); await pc.setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp: data.answer })); logMessage('info', 'WebRTC answer получен'); } catch (error) { logMessage('error', 'WebRTC ошибка: ' + error.message); } }); operatorSocket.on('webrtc:ice-candidate', async (data) => { try { logMessage('info', `📡 Получен ICE candidate: ${JSON.stringify(data.candidate)}`); const pc = getOrCreatePeerConnection(); const candidate = data.candidate; await pc.addIceCandidate(new RTCIceCandidate(candidate)); logMessage('success', '✅ ICE candidate добавлен успешно'); } catch (error) { logMessage('error', 'ICE candidate ошибка: ' + error.message); } }); } function getOrCreatePeerConnection() { if (!peerConnection) { peerConnection = new RTCPeerConnection(rtcConfig); peerConnection.onicecandidate = (event) => { if (event.candidate) { const candidateData = { candidate: event.candidate.candidate, sdpMid: event.candidate.sdpMid, sdpMLineIndex: event.candidate.sdpMLineIndex }; logMessage('info', `📤 Отправляем ICE candidate: ${JSON.stringify(candidateData)}`); if (androidSocket) { androidSocket.emit('webrtc:ice-candidate', { sessionId: currentSessionId, candidate: candidateData }); } else if (operatorSocket) { operatorSocket.emit('webrtc:ice-candidate', { sessionId: currentSessionId, candidate: candidateData }); } } }; peerConnection.ontrack = (event) => { const remoteVideo = document.getElementById('remoteVideo'); remoteVideo.srcObject = event.streams[0]; remoteVideo.style.display = 'block'; logMessage('info', 'Получен удаленный видеопоток'); }; peerConnection.onconnectionstatechange = () => { const state = peerConnection.connectionState; logMessage('info', `WebRTC состояние: ${state}`); const statusDiv = document.getElementById('webrtc-status'); statusDiv.innerHTML = `
WebRTC состояние: ${state}
`; }; } return peerConnection; } async function startLocalVideo() { try { localStream = await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 }, audio: true }); const localVideo = document.getElementById('localVideo'); localVideo.srcObject = localStream; const pc = getOrCreatePeerConnection(); localStream.getTracks().forEach(track => { pc.addTrack(track, localStream); }); logMessage('info', 'Локальное видео запущено'); } catch (error) { logMessage('error', 'Ошибка доступа к камере: ' + error.message); showAlert('danger', 'Ошибка доступа к камере: ' + error.message); } } function stopLocalVideo() { if (localStream) { localStream.getTracks().forEach(track => track.stop()); localStream = null; const localVideo = document.getElementById('localVideo'); localVideo.srcObject = null; logMessage('info', 'Локальное видео остановлено'); } if (peerConnection) { peerConnection.close(); peerConnection = null; const remoteVideo = document.getElementById('remoteVideo'); remoteVideo.srcObject = null; remoteVideo.style.display = 'none'; logMessage('info', 'WebRTC соединение закрыто'); } } // Вспомогательная функция для показа уведомлений function showAlert(type, message) { // Создаем временное уведомление const alert = document.createElement('div'); alert.className = `alert alert-${type}`; alert.textContent = message; alert.style.position = 'fixed'; alert.style.top = '20px'; alert.style.right = '20px'; alert.style.zIndex = '9999'; alert.style.minWidth = '300px'; document.body.appendChild(alert); // Удаляем через 5 секунд setTimeout(() => { if (document.body.contains(alert)) { document.body.removeChild(alert); } }, 5000); }