init commit

This commit is contained in:
2025-09-28 22:00:44 +09:00
commit 25cb9d9c8f
5877 changed files with 582116 additions and 0 deletions

803
backend/public/demo.js Normal file
View File

@@ -0,0 +1,803 @@
// Глобальные переменные
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) => {
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) {
const container = document.getElementById('android-requests');
const requestDiv = document.createElement('div');
requestDiv.className = 'session-card session-pending';
requestDiv.innerHTML = `
<h4>Запрос камеры</h4>
<p><strong>Сессия:</strong> ${data.sessionId}</p>
<p><strong>Оператор:</strong> ${data.operatorId}</p>
<p><strong>Камера:</strong> ${data.cameraType}</p>
<button class="btn btn-success" onclick="acceptCameraRequest('${data.sessionId}')">Принять</button>
<button class="btn btn-danger" onclick="declineCameraRequest('${data.sessionId}')">Отклонить</button>
`;
container.appendChild(requestDiv);
logMessage('info', `Получен запрос камеры от ${data.operatorId}`);
}
function acceptCameraRequest(sessionId) {
if (androidSocket) {
androidSocket.emit('camera:response', {
sessionId,
accepted: true,
streamUrl: 'webrtc'
});
// Перемещаем в активные сессии
moveToActiveSessions(sessionId, 'active');
logMessage('info', `Запрос камеры принят: ${sessionId}`);
}
}
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 = `
<h4>Активная сессия</h4>
<p><strong>ID:</strong> ${sessionId}</p>
<p><strong>Статус:</strong> ${status}</p>
<button class="btn btn-danger" onclick="endAndroidSession('${sessionId}')">Завершить</button>
`;
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 = `
<h4>${device.deviceInfo.model || 'Unknown Device'}</h4>
<p><strong>ID:</strong> ${device.deviceId}</p>
<p><strong>Статус:</strong> ${device.status}</p>
<p><strong>Камеры:</strong> ${device.capabilities?.cameras?.join(', ') || 'Unknown'}</p>
${device.canAcceptSession ?
`<select id="camera-${device.deviceId}">
${device.capabilities?.cameras?.map(camera =>
`<option value="${camera}">${getCameraName(camera)}</option>`
).join('') || '<option value="back">Основная</option>'}
</select>
<button class="btn" onclick="requestCamera('${device.deviceId}')">Запросить камеру</button>`
: '<p style="color: #666;">Недоступен для новых сессий</p>'
}
`;
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 = `
<h4>Сессия камеры</h4>
<p><strong>ID:</strong> ${sessionId}</p>
<p><strong>Статус:</strong> <span class="session-status">${status}</span></p>
<button class="btn" onclick="switchCamera('${sessionId}', 'front')">Фронтальная</button>
<button class="btn" onclick="switchCamera('${sessionId}', 'back')">Основная</button>
<button class="btn btn-danger" onclick="endOperatorSession('${sessionId}')">Завершить</button>
`;
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 = `
<div class="alert alert-${data.health.status === 'healthy' ? 'success' : 'warning'}">
<h4>Состояние системы: ${data.health.status}</h4>
<p><strong>Память:</strong> ${data.health.memory.used}MB / ${data.health.memory.total}MB</p>
<p><strong>Время работы:</strong> ${data.health.uptime} сек</p>
<p><strong>Подключения:</strong> ${data.health.connections.devices} устройств, ${data.health.connections.operators} операторов</p>
${data.health.warnings ? `<p><strong>Предупреждения:</strong> ${data.health.warnings.join(', ')}</p>` : ''}
</div>
`;
} 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 = `
<div class="alert alert-info">
<h4>Детальная статистика</h4>
<pre>${JSON.stringify(data.stats, null, 2)}</pre>
</div>
`;
} 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 = `
<h5>${device.deviceInfo.model || 'Unknown Device'}</h5>
<p><strong>ID:</strong> ${device.deviceId}</p>
<p><strong>Статус:</strong> ${device.status}</p>
<p><strong>Активных сессий:</strong> ${device.activeSessions}</p>
<p><strong>Время работы:</strong> ${device.uptime} сек</p>
`;
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 = `
<h5>Сессия ${session.sessionId}</h5>
<p><strong>Устройство:</strong> ${session.deviceId}</p>
<p><strong>Оператор:</strong> ${session.operatorId}</p>
<p><strong>Статус:</strong> ${session.status}</p>
<p><strong>Продолжительность:</strong> ${session.duration}</p>
<p><strong>Камера:</strong> ${getCameraName(session.cameraType)}</p>
`;
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 {
const pc = getOrCreatePeerConnection();
const candidate = JSON.parse(data.candidate);
await pc.addIceCandidate(new RTCIceCandidate(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 {
const pc = getOrCreatePeerConnection();
const candidate = JSON.parse(data.candidate);
await pc.addIceCandidate(new RTCIceCandidate(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 = JSON.stringify({
candidate: event.candidate.candidate,
sdpMid: event.candidate.sdpMid,
sdpMLineIndex: event.candidate.sdpMLineIndex
});
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 = `
<div class="alert alert-info">
<strong>WebRTC состояние:</strong> ${state}
</div>
`;
};
}
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);
}