init commit
This commit is contained in:
453
backend/src/managers/DeviceManager.js
Normal file
453
backend/src/managers/DeviceManager.js
Normal file
@@ -0,0 +1,453 @@
|
||||
class DeviceManager {
|
||||
constructor() {
|
||||
this.devices = new Map(); // deviceId -> DeviceInfo
|
||||
this.operators = new Map(); // operatorId -> OperatorInfo
|
||||
}
|
||||
|
||||
/**
|
||||
* Регистрация Android устройства
|
||||
* @param {string} deviceId
|
||||
* @param {object} deviceInfo
|
||||
* @param {object} socket
|
||||
* @returns {DeviceInfo}
|
||||
*/
|
||||
registerDevice(deviceId, deviceInfo, socket) {
|
||||
const device = new DeviceInfo(deviceId, deviceInfo, socket);
|
||||
this.devices.set(deviceId, device);
|
||||
return device;
|
||||
}
|
||||
|
||||
/**
|
||||
* Регистрация оператора
|
||||
* @param {string} operatorId
|
||||
* @param {object} operatorInfo
|
||||
* @param {object} socket
|
||||
* @returns {OperatorInfo}
|
||||
*/
|
||||
registerOperator(operatorId, operatorInfo, socket) {
|
||||
const operator = new OperatorInfo(operatorId, operatorInfo, socket);
|
||||
this.operators.set(operatorId, operator);
|
||||
return operator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение устройства
|
||||
* @param {string} deviceId
|
||||
* @returns {DeviceInfo|null}
|
||||
*/
|
||||
getDevice(deviceId) {
|
||||
return this.devices.get(deviceId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение оператора
|
||||
* @param {string} operatorId
|
||||
* @returns {OperatorInfo|null}
|
||||
*/
|
||||
getOperator(operatorId) {
|
||||
return this.operators.get(operatorId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение всех подключенных устройств
|
||||
* @returns {DeviceInfo[]}
|
||||
*/
|
||||
getConnectedDevices() {
|
||||
return Array.from(this.devices.values()).filter(device => device.isConnected());
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение всех подключенных операторов
|
||||
* @returns {OperatorInfo[]}
|
||||
*/
|
||||
getConnectedOperators() {
|
||||
return Array.from(this.operators.values()).filter(operator => operator.isConnected());
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение доступных устройств для оператора
|
||||
* @param {string} operatorId
|
||||
* @returns {DeviceInfo[]}
|
||||
*/
|
||||
getAvailableDevicesForOperator(operatorId) {
|
||||
return this.getConnectedDevices().filter(device => {
|
||||
// Устройство доступно если оно подключено и не занято активной сессией
|
||||
return device.isConnected() && device.canAcceptNewSession();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Отключение устройства
|
||||
* @param {string} deviceId
|
||||
*/
|
||||
disconnectDevice(deviceId) {
|
||||
const device = this.devices.get(deviceId);
|
||||
if (device) {
|
||||
device.disconnect();
|
||||
this.devices.delete(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отключение оператора
|
||||
* @param {string} operatorId
|
||||
*/
|
||||
disconnectOperator(operatorId) {
|
||||
const operator = this.operators.get(operatorId);
|
||||
if (operator) {
|
||||
operator.disconnect();
|
||||
this.operators.delete(operatorId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление местоположения устройства
|
||||
* @param {string} deviceId
|
||||
* @param {object} location
|
||||
*/
|
||||
updateDeviceLocation(deviceId, location) {
|
||||
const device = this.devices.get(deviceId);
|
||||
if (device) {
|
||||
device.updateLocation(location);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление статуса устройства
|
||||
* @param {string} deviceId
|
||||
* @param {string} status
|
||||
*/
|
||||
updateDeviceStatus(deviceId, status) {
|
||||
const device = this.devices.get(deviceId);
|
||||
if (device) {
|
||||
device.updateStatus(status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение статистики
|
||||
* @returns {object}
|
||||
*/
|
||||
getStats() {
|
||||
const devices = Array.from(this.devices.values());
|
||||
const operators = Array.from(this.operators.values());
|
||||
|
||||
return {
|
||||
totalDevices: devices.length,
|
||||
connectedDevices: devices.filter(d => d.isConnected()).length,
|
||||
totalOperators: operators.length,
|
||||
connectedOperators: operators.filter(o => o.isConnected()).length,
|
||||
devicesByPlatform: this.getDevicesByPlatform(),
|
||||
devicesByStatus: this.getDevicesByStatus(),
|
||||
averageUptime: this.calculateAverageUptime()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Статистика устройств по платформе
|
||||
* @returns {object}
|
||||
*/
|
||||
getDevicesByPlatform() {
|
||||
const stats = {};
|
||||
Array.from(this.devices.values()).forEach(device => {
|
||||
const platform = device.deviceInfo.manufacturer || 'unknown';
|
||||
stats[platform] = (stats[platform] || 0) + 1;
|
||||
});
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Статистика устройств по статусу
|
||||
* @returns {object}
|
||||
*/
|
||||
getDevicesByStatus() {
|
||||
const stats = {};
|
||||
Array.from(this.devices.values()).forEach(device => {
|
||||
stats[device.status] = (stats[device.status] || 0) + 1;
|
||||
});
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Расчет среднего времени работы
|
||||
* @returns {number}
|
||||
*/
|
||||
calculateAverageUptime() {
|
||||
const devices = Array.from(this.devices.values());
|
||||
if (devices.length === 0) return 0;
|
||||
|
||||
const totalUptime = devices.reduce((sum, device) => sum + device.getUptime(), 0);
|
||||
return Math.round(totalUptime / devices.length / 1000); // в секундах
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистка отключенных устройств и операторов
|
||||
*/
|
||||
cleanup() {
|
||||
// Удаляем отключенные устройства старше 5 минут
|
||||
const cutoffTime = Date.now() - (5 * 60 * 1000);
|
||||
|
||||
for (const [deviceId, device] of this.devices) {
|
||||
if (!device.isConnected() && device.lastSeen < cutoffTime) {
|
||||
this.devices.delete(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [operatorId, operator] of this.operators) {
|
||||
if (!operator.isConnected() && operator.lastSeen < cutoffTime) {
|
||||
this.operators.delete(operatorId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DeviceInfo {
|
||||
constructor(deviceId, deviceInfo, socket) {
|
||||
this.deviceId = deviceId;
|
||||
this.deviceInfo = deviceInfo;
|
||||
this.socket = socket;
|
||||
this.status = 'connected';
|
||||
this.connectedAt = Date.now();
|
||||
this.lastSeen = Date.now();
|
||||
this.location = null;
|
||||
this.activeSessions = new Set();
|
||||
this.totalSessions = 0;
|
||||
this.capabilities = this.parseCapabilities(deviceInfo);
|
||||
this.healthCheck = {
|
||||
lastPing: Date.now(),
|
||||
responseTime: null,
|
||||
errors: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Парсинг возможностей устройства
|
||||
* @param {object} deviceInfo
|
||||
* @returns {object}
|
||||
*/
|
||||
parseCapabilities(deviceInfo) {
|
||||
const availableCameras = deviceInfo.availableCameras?.split(',') || ['back'];
|
||||
|
||||
return {
|
||||
cameras: availableCameras,
|
||||
hasMultipleCameras: availableCameras.length > 1,
|
||||
hasUltraWide: availableCameras.includes('ultra_wide'),
|
||||
hasTelephoto: availableCameras.includes('telephoto'),
|
||||
supportedResolutions: deviceInfo.supportedResolutions || ['720p', '1080p'],
|
||||
maxConcurrentSessions: deviceInfo.maxConcurrentSessions || 1
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка подключения
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isConnected() {
|
||||
return this.socket && this.socket.connected && this.status === 'connected';
|
||||
}
|
||||
|
||||
/**
|
||||
* Может ли устройство принять новую сессию
|
||||
* @returns {boolean}
|
||||
*/
|
||||
canAcceptNewSession() {
|
||||
return this.isConnected() &&
|
||||
this.activeSessions.size < this.capabilities.maxConcurrentSessions &&
|
||||
this.status === 'connected';
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавление активной сессии
|
||||
* @param {string} sessionId
|
||||
*/
|
||||
addSession(sessionId) {
|
||||
this.activeSessions.add(sessionId);
|
||||
this.totalSessions++;
|
||||
this.updateStatus('busy');
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаление активной сессии
|
||||
* @param {string} sessionId
|
||||
*/
|
||||
removeSession(sessionId) {
|
||||
this.activeSessions.delete(sessionId);
|
||||
|
||||
if (this.activeSessions.size === 0) {
|
||||
this.updateStatus('connected');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление статуса
|
||||
* @param {string} status
|
||||
*/
|
||||
updateStatus(status) {
|
||||
this.status = status;
|
||||
this.lastSeen = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление местоположения
|
||||
* @param {object} location
|
||||
*/
|
||||
updateLocation(location) {
|
||||
this.location = {
|
||||
latitude: location.latitude,
|
||||
longitude: location.longitude,
|
||||
accuracy: location.accuracy,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Отключение устройства
|
||||
*/
|
||||
disconnect() {
|
||||
this.status = 'disconnected';
|
||||
this.lastSeen = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение времени работы
|
||||
* @returns {number}
|
||||
*/
|
||||
getUptime() {
|
||||
return Date.now() - this.connectedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление health check
|
||||
* @param {number} responseTime
|
||||
*/
|
||||
updateHealthCheck(responseTime) {
|
||||
this.healthCheck.lastPing = Date.now();
|
||||
this.healthCheck.responseTime = responseTime;
|
||||
this.healthCheck.errors = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Регистрация ошибки
|
||||
*/
|
||||
recordError() {
|
||||
this.healthCheck.errors++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение сводки устройства
|
||||
* @returns {object}
|
||||
*/
|
||||
getSummary() {
|
||||
return {
|
||||
deviceId: this.deviceId,
|
||||
deviceInfo: this.deviceInfo,
|
||||
status: this.status,
|
||||
isConnected: this.isConnected(),
|
||||
connectedAt: new Date(this.connectedAt).toISOString(),
|
||||
lastSeen: new Date(this.lastSeen).toISOString(),
|
||||
uptime: Math.round(this.getUptime() / 1000), // в секундах
|
||||
location: this.location,
|
||||
capabilities: this.capabilities,
|
||||
activeSessions: this.activeSessions.size,
|
||||
totalSessions: this.totalSessions,
|
||||
canAcceptSession: this.canAcceptNewSession(),
|
||||
healthCheck: this.healthCheck
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class OperatorInfo {
|
||||
constructor(operatorId, operatorInfo, socket) {
|
||||
this.operatorId = operatorId;
|
||||
this.operatorInfo = operatorInfo || {};
|
||||
this.socket = socket;
|
||||
this.status = 'connected';
|
||||
this.connectedAt = Date.now();
|
||||
this.lastSeen = Date.now();
|
||||
this.activeSessions = new Set();
|
||||
this.totalSessions = 0;
|
||||
this.permissions = this.operatorInfo.permissions || ['view_cameras', 'request_camera'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка подключения
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isConnected() {
|
||||
return this.socket && this.socket.connected && this.status === 'connected';
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка разрешений
|
||||
* @param {string} permission
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasPermission(permission) {
|
||||
return this.permissions.includes(permission) || this.permissions.includes('admin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавление активной сессии
|
||||
* @param {string} sessionId
|
||||
*/
|
||||
addSession(sessionId) {
|
||||
this.activeSessions.add(sessionId);
|
||||
this.totalSessions++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаление активной сессии
|
||||
* @param {string} sessionId
|
||||
*/
|
||||
removeSession(sessionId) {
|
||||
this.activeSessions.delete(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление последнего времени активности
|
||||
*/
|
||||
updateActivity() {
|
||||
this.lastSeen = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Отключение оператора
|
||||
*/
|
||||
disconnect() {
|
||||
this.status = 'disconnected';
|
||||
this.lastSeen = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение времени работы
|
||||
* @returns {number}
|
||||
*/
|
||||
getUptime() {
|
||||
return Date.now() - this.connectedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение сводки оператора
|
||||
* @returns {object}
|
||||
*/
|
||||
getSummary() {
|
||||
return {
|
||||
operatorId: this.operatorId,
|
||||
operatorInfo: this.operatorInfo,
|
||||
status: this.status,
|
||||
isConnected: this.isConnected(),
|
||||
connectedAt: new Date(this.connectedAt).toISOString(),
|
||||
lastSeen: new Date(this.lastSeen).toISOString(),
|
||||
uptime: Math.round(this.getUptime() / 1000), // в секундах
|
||||
activeSessions: this.activeSessions.size,
|
||||
totalSessions: this.totalSessions,
|
||||
permissions: this.permissions
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DeviceManager,
|
||||
DeviceInfo,
|
||||
OperatorInfo
|
||||
};
|
||||
385
backend/src/managers/SessionManager.js
Normal file
385
backend/src/managers/SessionManager.js
Normal file
@@ -0,0 +1,385 @@
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
class SessionManager {
|
||||
constructor() {
|
||||
this.sessions = new Map(); // sessionId -> SessionInfo
|
||||
this.deviceSessions = new Map(); // deviceId -> Set(sessionIds)
|
||||
this.operatorSessions = new Map(); // operatorId -> Set(sessionIds)
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание новой сессии
|
||||
* @param {string} deviceId
|
||||
* @param {string} operatorId
|
||||
* @param {string} cameraType
|
||||
* @returns {SessionInfo}
|
||||
*/
|
||||
createSession(deviceId, operatorId, cameraType = 'back') {
|
||||
const sessionId = uuidv4();
|
||||
const session = new SessionInfo(sessionId, deviceId, operatorId, cameraType);
|
||||
|
||||
this.sessions.set(sessionId, session);
|
||||
|
||||
// Добавляем к устройству
|
||||
if (!this.deviceSessions.has(deviceId)) {
|
||||
this.deviceSessions.set(deviceId, new Set());
|
||||
}
|
||||
this.deviceSessions.get(deviceId).add(sessionId);
|
||||
|
||||
// Добавляем к оператору
|
||||
if (!this.operatorSessions.has(operatorId)) {
|
||||
this.operatorSessions.set(operatorId, new Set());
|
||||
}
|
||||
this.operatorSessions.get(operatorId).add(sessionId);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение сессии по ID
|
||||
* @param {string} sessionId
|
||||
* @returns {SessionInfo|null}
|
||||
*/
|
||||
getSession(sessionId) {
|
||||
return this.sessions.get(sessionId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение всех сессий устройства
|
||||
* @param {string} deviceId
|
||||
* @returns {SessionInfo[]}
|
||||
*/
|
||||
getDeviceSessions(deviceId) {
|
||||
const sessionIds = this.deviceSessions.get(deviceId);
|
||||
if (!sessionIds) return [];
|
||||
|
||||
return Array.from(sessionIds)
|
||||
.map(id => this.sessions.get(id))
|
||||
.filter(session => session !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение всех сессий оператора
|
||||
* @param {string} operatorId
|
||||
* @returns {SessionInfo[]}
|
||||
*/
|
||||
getOperatorSessions(operatorId) {
|
||||
const sessionIds = this.operatorSessions.get(operatorId);
|
||||
if (!sessionIds) return [];
|
||||
|
||||
return Array.from(sessionIds)
|
||||
.map(id => this.sessions.get(id))
|
||||
.filter(session => session !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление состояния сессии
|
||||
* @param {string} sessionId
|
||||
* @param {string} status
|
||||
* @param {object} metadata
|
||||
*/
|
||||
updateSession(sessionId, status, metadata = {}) {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (session) {
|
||||
session.updateStatus(status, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Завершение сессии
|
||||
* @param {string} sessionId
|
||||
* @returns {SessionInfo|null}
|
||||
*/
|
||||
closeSession(sessionId) {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return null;
|
||||
|
||||
session.close();
|
||||
|
||||
// Удаляем из устройства
|
||||
const deviceSessions = this.deviceSessions.get(session.deviceId);
|
||||
if (deviceSessions) {
|
||||
deviceSessions.delete(sessionId);
|
||||
if (deviceSessions.size === 0) {
|
||||
this.deviceSessions.delete(session.deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
// Удаляем из оператора
|
||||
const operatorSessions = this.operatorSessions.get(session.operatorId);
|
||||
if (operatorSessions) {
|
||||
operatorSessions.delete(sessionId);
|
||||
if (operatorSessions.size === 0) {
|
||||
this.operatorSessions.delete(session.operatorId);
|
||||
}
|
||||
}
|
||||
|
||||
this.sessions.delete(sessionId);
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Закрытие всех сессий устройства
|
||||
* @param {string} deviceId
|
||||
*/
|
||||
closeDeviceSessions(deviceId) {
|
||||
const sessions = this.getDeviceSessions(deviceId);
|
||||
sessions.forEach(session => this.closeSession(session.sessionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Закрытие всех сессий оператора
|
||||
* @param {string} operatorId
|
||||
*/
|
||||
closeOperatorSessions(operatorId) {
|
||||
const sessions = this.getOperatorSessions(operatorId);
|
||||
sessions.forEach(session => this.closeSession(session.sessionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение статистики
|
||||
* @returns {object}
|
||||
*/
|
||||
getStats() {
|
||||
const sessions = Array.from(this.sessions.values());
|
||||
|
||||
return {
|
||||
totalSessions: sessions.length,
|
||||
activeSessions: sessions.filter(s => s.status === 'active').length,
|
||||
pendingSessions: sessions.filter(s => s.status === 'pending').length,
|
||||
closedSessions: sessions.filter(s => s.status === 'closed').length,
|
||||
devices: this.deviceSessions.size,
|
||||
operators: this.operatorSessions.size,
|
||||
avgSessionDuration: this.calculateAverageSessionDuration(),
|
||||
sessionsByCamera: this.getSessionsByCameraType()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Расчет средней продолжительности сессий
|
||||
* @returns {number}
|
||||
*/
|
||||
calculateAverageSessionDuration() {
|
||||
const closedSessions = Array.from(this.sessions.values())
|
||||
.filter(s => s.status === 'closed' && s.endTime);
|
||||
|
||||
if (closedSessions.length === 0) return 0;
|
||||
|
||||
const totalDuration = closedSessions.reduce((sum, session) => {
|
||||
return sum + (session.endTime - session.startTime);
|
||||
}, 0);
|
||||
|
||||
return Math.round(totalDuration / closedSessions.length / 1000); // в секундах
|
||||
}
|
||||
|
||||
/**
|
||||
* Статистика по типам камер
|
||||
* @returns {object}
|
||||
*/
|
||||
getSessionsByCameraType() {
|
||||
const stats = {};
|
||||
Array.from(this.sessions.values()).forEach(session => {
|
||||
stats[session.cameraType] = (stats[session.cameraType] || 0) + 1;
|
||||
});
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение всех активных сессий
|
||||
* @returns {SessionInfo[]}
|
||||
*/
|
||||
getActiveSessions() {
|
||||
return Array.from(this.sessions.values())
|
||||
.filter(session => session.status === 'active');
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистка старых закрытых сессий (старше N минут)
|
||||
* @param {number} maxAgeMinutes
|
||||
*/
|
||||
cleanupOldSessions(maxAgeMinutes = 60) {
|
||||
const cutoffTime = Date.now() - (maxAgeMinutes * 60 * 1000);
|
||||
const sessionsToRemove = [];
|
||||
|
||||
for (const [sessionId, session] of this.sessions) {
|
||||
if (session.status === 'closed' && session.endTime < cutoffTime) {
|
||||
sessionsToRemove.push(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
sessionsToRemove.forEach(sessionId => {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (session) {
|
||||
this.sessions.delete(sessionId);
|
||||
|
||||
// Очистка из устройств и операторов
|
||||
const deviceSessions = this.deviceSessions.get(session.deviceId);
|
||||
if (deviceSessions) {
|
||||
deviceSessions.delete(sessionId);
|
||||
}
|
||||
|
||||
const operatorSessions = this.operatorSessions.get(session.operatorId);
|
||||
if (operatorSessions) {
|
||||
operatorSessions.delete(sessionId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return sessionsToRemove.length;
|
||||
}
|
||||
}
|
||||
|
||||
class SessionInfo {
|
||||
constructor(sessionId, deviceId, operatorId, cameraType) {
|
||||
this.sessionId = sessionId;
|
||||
this.deviceId = deviceId;
|
||||
this.operatorId = operatorId;
|
||||
this.cameraType = cameraType;
|
||||
this.status = 'pending'; // pending, active, closed, error
|
||||
this.startTime = Date.now();
|
||||
this.endTime = null;
|
||||
this.metadata = {};
|
||||
this.events = [];
|
||||
this.webrtcState = null;
|
||||
this.quality = null;
|
||||
this.bandwidth = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление статуса сессии
|
||||
* @param {string} status
|
||||
* @param {object} metadata
|
||||
*/
|
||||
updateStatus(status, metadata = {}) {
|
||||
this.status = status;
|
||||
this.metadata = { ...this.metadata, ...metadata };
|
||||
|
||||
this.events.push({
|
||||
timestamp: Date.now(),
|
||||
event: 'status_change',
|
||||
data: { status, metadata }
|
||||
});
|
||||
|
||||
if (status === 'active' && !this.startTime) {
|
||||
this.startTime = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление WebRTC состояния
|
||||
* @param {string} state
|
||||
*/
|
||||
updateWebRTCState(state) {
|
||||
this.webrtcState = state;
|
||||
this.events.push({
|
||||
timestamp: Date.now(),
|
||||
event: 'webrtc_state_change',
|
||||
data: { state }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление качества потока
|
||||
* @param {object} qualityData
|
||||
*/
|
||||
updateQuality(qualityData) {
|
||||
this.quality = qualityData;
|
||||
this.events.push({
|
||||
timestamp: Date.now(),
|
||||
event: 'quality_update',
|
||||
data: qualityData
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Смена типа камеры
|
||||
* @param {string} newCameraType
|
||||
*/
|
||||
switchCamera(newCameraType) {
|
||||
const oldType = this.cameraType;
|
||||
this.cameraType = newCameraType;
|
||||
|
||||
this.events.push({
|
||||
timestamp: Date.now(),
|
||||
event: 'camera_switch',
|
||||
data: { from: oldType, to: newCameraType }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Закрытие сессии
|
||||
*/
|
||||
close() {
|
||||
this.status = 'closed';
|
||||
this.endTime = Date.now();
|
||||
|
||||
this.events.push({
|
||||
timestamp: Date.now(),
|
||||
event: 'session_closed',
|
||||
data: { duration: this.getDuration() }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение продолжительности сессии в миллисекундах
|
||||
* @returns {number}
|
||||
*/
|
||||
getDuration() {
|
||||
const endTime = this.endTime || Date.now();
|
||||
return endTime - this.startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение человекочитаемой продолжительности
|
||||
* @returns {string}
|
||||
*/
|
||||
getFormattedDuration() {
|
||||
const duration = this.getDuration();
|
||||
const seconds = Math.floor(duration / 1000) % 60;
|
||||
const minutes = Math.floor(duration / (1000 * 60)) % 60;
|
||||
const hours = Math.floor(duration / (1000 * 60 * 60));
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение сводки сессии
|
||||
* @returns {object}
|
||||
*/
|
||||
getSummary() {
|
||||
return {
|
||||
sessionId: this.sessionId,
|
||||
deviceId: this.deviceId,
|
||||
operatorId: this.operatorId,
|
||||
cameraType: this.cameraType,
|
||||
status: this.status,
|
||||
duration: this.getFormattedDuration(),
|
||||
durationMs: this.getDuration(),
|
||||
startTime: new Date(this.startTime).toISOString(),
|
||||
endTime: this.endTime ? new Date(this.endTime).toISOString() : null,
|
||||
webrtcState: this.webrtcState,
|
||||
quality: this.quality,
|
||||
eventsCount: this.events.length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение полной информации о сессии
|
||||
* @returns {object}
|
||||
*/
|
||||
getFullInfo() {
|
||||
return {
|
||||
...this.getSummary(),
|
||||
metadata: this.metadata,
|
||||
events: this.events
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SessionManager,
|
||||
SessionInfo
|
||||
};
|
||||
411
backend/src/routes/admin.js
Normal file
411
backend/src/routes/admin.js
Normal file
@@ -0,0 +1,411 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* GET /api/admin/stats
|
||||
* Получение общей статистики системы
|
||||
*/
|
||||
router.get('/stats', (req, res) => {
|
||||
try {
|
||||
const { deviceManager, sessionManager } = req.app.locals;
|
||||
|
||||
const deviceStats = deviceManager.getStats();
|
||||
const sessionStats = sessionManager.getStats();
|
||||
|
||||
const systemStats = {
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: Math.round(process.uptime()),
|
||||
memory: process.memoryUsage(),
|
||||
devices: deviceStats,
|
||||
sessions: sessionStats,
|
||||
system: {
|
||||
nodeVersion: process.version,
|
||||
platform: process.platform,
|
||||
arch: process.arch
|
||||
}
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
stats: systemStats
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/admin/devices
|
||||
* Получение списка всех устройств (включая отключенные)
|
||||
*/
|
||||
router.get('/devices', (req, res) => {
|
||||
try {
|
||||
const { deviceManager } = req.app.locals;
|
||||
const { status, limit = 100, offset = 0 } = req.query;
|
||||
|
||||
let devices = Array.from(deviceManager.devices.values());
|
||||
|
||||
// Фильтрация по статусу
|
||||
if (status) {
|
||||
devices = devices.filter(device => device.status === status);
|
||||
}
|
||||
|
||||
// Пагинация
|
||||
const total = devices.length;
|
||||
devices = devices.slice(offset, offset + parseInt(limit));
|
||||
|
||||
const devicesData = devices.map(device => device.getSummary());
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
devices: devicesData,
|
||||
pagination: {
|
||||
total,
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
hasMore: (offset + parseInt(limit)) < total
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/admin/operators
|
||||
* Получение списка всех операторов
|
||||
*/
|
||||
router.get('/operators', (req, res) => {
|
||||
try {
|
||||
const { deviceManager } = req.app.locals;
|
||||
const { status, limit = 100, offset = 0 } = req.query;
|
||||
|
||||
let operators = Array.from(deviceManager.operators.values());
|
||||
|
||||
// Фильтрация по статусу
|
||||
if (status) {
|
||||
operators = operators.filter(operator => operator.status === status);
|
||||
}
|
||||
|
||||
// Пагинация
|
||||
const total = operators.length;
|
||||
operators = operators.slice(offset, offset + parseInt(limit));
|
||||
|
||||
const operatorsData = operators.map(operator => operator.getSummary());
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
operators: operatorsData,
|
||||
pagination: {
|
||||
total,
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
hasMore: (offset + parseInt(limit)) < total
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/admin/sessions
|
||||
* Получение списка всех сессий
|
||||
*/
|
||||
router.get('/sessions', (req, res) => {
|
||||
try {
|
||||
const { sessionManager } = req.app.locals;
|
||||
const { status, deviceId, operatorId, limit = 100, offset = 0 } = req.query;
|
||||
|
||||
let sessions = Array.from(sessionManager.sessions.values());
|
||||
|
||||
// Фильтрация
|
||||
if (status) {
|
||||
sessions = sessions.filter(session => session.status === status);
|
||||
}
|
||||
if (deviceId) {
|
||||
sessions = sessions.filter(session => session.deviceId === deviceId);
|
||||
}
|
||||
if (operatorId) {
|
||||
sessions = sessions.filter(session => session.operatorId === operatorId);
|
||||
}
|
||||
|
||||
// Сортировка по времени создания (новые первыми)
|
||||
sessions.sort((a, b) => b.startTime - a.startTime);
|
||||
|
||||
// Пагинация
|
||||
const total = sessions.length;
|
||||
sessions = sessions.slice(offset, offset + parseInt(limit));
|
||||
|
||||
const sessionsData = sessions.map(session => session.getSummary());
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
sessions: sessionsData,
|
||||
pagination: {
|
||||
total,
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
hasMore: (offset + parseInt(limit)) < total
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/admin/sessions/:sessionId
|
||||
* Получение детальной информации о сессии (включая события)
|
||||
*/
|
||||
router.get('/sessions/:sessionId', (req, res) => {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
const { sessionManager } = req.app.locals;
|
||||
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
if (!session) {
|
||||
return res.status(404).json({ error: 'Session not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
session: session.getFullInfo()
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/admin/sessions/:sessionId/terminate
|
||||
* Принудительное завершение сессии
|
||||
*/
|
||||
router.post('/sessions/:sessionId/terminate', (req, res) => {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
const { reason = 'Terminated by admin' } = req.body;
|
||||
|
||||
const { sessionManager, deviceManager } = req.app.locals;
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
|
||||
if (!session) {
|
||||
return res.status(404).json({ error: 'Session not found' });
|
||||
}
|
||||
|
||||
const device = deviceManager.getDevice(session.deviceId);
|
||||
const operator = deviceManager.getOperator(session.operatorId);
|
||||
|
||||
// Уведомляем участников
|
||||
if (device && device.isConnected()) {
|
||||
device.socket.emit('camera:disconnect', { sessionId, reason });
|
||||
device.removeSession(sessionId);
|
||||
}
|
||||
|
||||
if (operator && operator.isConnected()) {
|
||||
operator.socket.emit('camera:disconnected', { sessionId, reason });
|
||||
operator.removeSession(sessionId);
|
||||
}
|
||||
|
||||
// Закрываем сессию
|
||||
session.updateStatus('terminated', { reason, terminatedBy: 'admin' });
|
||||
sessionManager.closeSession(sessionId);
|
||||
|
||||
req.app.locals.logger.warn(`Session ${sessionId} terminated by admin: ${reason}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Session terminated',
|
||||
sessionId
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
req.app.locals.logger.error('Error terminating session', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/admin/devices/:deviceId/disconnect
|
||||
* Принудительное отключение устройства
|
||||
*/
|
||||
router.post('/devices/:deviceId/disconnect', (req, res) => {
|
||||
try {
|
||||
const { deviceId } = req.params;
|
||||
const { reason = 'Disconnected by admin' } = req.body;
|
||||
|
||||
const { deviceManager, sessionManager } = req.app.locals;
|
||||
const device = deviceManager.getDevice(deviceId);
|
||||
|
||||
if (!device) {
|
||||
return res.status(404).json({ error: 'Device not found' });
|
||||
}
|
||||
|
||||
// Завершаем все активные сессии устройства
|
||||
const deviceSessions = sessionManager.getDeviceSessions(deviceId);
|
||||
deviceSessions.forEach(session => {
|
||||
if (session.status === 'active' || session.status === 'pending') {
|
||||
const operator = deviceManager.getOperator(session.operatorId);
|
||||
if (operator && operator.isConnected()) {
|
||||
operator.socket.emit('camera:disconnected', {
|
||||
sessionId: session.sessionId,
|
||||
reason: `Device disconnected: ${reason}`
|
||||
});
|
||||
operator.removeSession(session.sessionId);
|
||||
}
|
||||
|
||||
session.updateStatus('terminated', { reason, terminatedBy: 'admin' });
|
||||
sessionManager.closeSession(session.sessionId);
|
||||
}
|
||||
});
|
||||
|
||||
// Отключаем устройство
|
||||
if (device.isConnected()) {
|
||||
device.socket.emit('force:disconnect', { reason });
|
||||
device.socket.disconnect();
|
||||
}
|
||||
|
||||
deviceManager.disconnectDevice(deviceId);
|
||||
|
||||
req.app.locals.logger.warn(`Device ${deviceId} disconnected by admin: ${reason}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Device disconnected',
|
||||
deviceId,
|
||||
terminatedSessions: deviceSessions.length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
req.app.locals.logger.error('Error disconnecting device', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/admin/cleanup
|
||||
* Очистка старых сессий и отключенных устройств
|
||||
*/
|
||||
router.post('/cleanup', (req, res) => {
|
||||
try {
|
||||
const { maxAgeMinutes = 60 } = req.body;
|
||||
|
||||
const { deviceManager, sessionManager } = req.app.locals;
|
||||
|
||||
// Очистка старых сессий
|
||||
const cleanedSessions = sessionManager.cleanupOldSessions(maxAgeMinutes);
|
||||
|
||||
// Очистка отключенных устройств
|
||||
deviceManager.cleanup();
|
||||
|
||||
req.app.locals.logger.info(`Cleanup completed: ${cleanedSessions} sessions removed`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Cleanup completed',
|
||||
removedSessions: cleanedSessions
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
req.app.locals.logger.error('Error during cleanup', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/admin/health
|
||||
* Проверка состояния системы
|
||||
*/
|
||||
router.get('/health', (req, res) => {
|
||||
try {
|
||||
const { deviceManager, sessionManager } = req.app.locals;
|
||||
|
||||
const health = {
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: Math.round(process.uptime()),
|
||||
memory: {
|
||||
used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
||||
total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024),
|
||||
external: Math.round(process.memoryUsage().external / 1024 / 1024)
|
||||
},
|
||||
connections: {
|
||||
devices: deviceManager.getConnectedDevices().length,
|
||||
operators: deviceManager.getConnectedOperators().length,
|
||||
activeSessions: sessionManager.getActiveSessions().length
|
||||
}
|
||||
};
|
||||
|
||||
// Проверка на критические состояния
|
||||
if (health.memory.used / health.memory.total > 0.9) {
|
||||
health.status = 'warning';
|
||||
health.warnings = health.warnings || [];
|
||||
health.warnings.push('High memory usage');
|
||||
}
|
||||
|
||||
if (health.uptime < 60) {
|
||||
health.status = 'warning';
|
||||
health.warnings = health.warnings || [];
|
||||
health.warnings.push('Recently restarted');
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
health
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
health: {
|
||||
status: 'unhealthy',
|
||||
error: error.message,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/admin/logs
|
||||
* Получение логов системы (последние записи)
|
||||
*/
|
||||
router.get('/logs', (req, res) => {
|
||||
try {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { lines = 100 } = req.query;
|
||||
|
||||
const logFile = path.join(process.cwd(), 'god-eye.log');
|
||||
|
||||
if (!fs.existsSync(logFile)) {
|
||||
return res.json({
|
||||
success: true,
|
||||
logs: [],
|
||||
message: 'Log file not found'
|
||||
});
|
||||
}
|
||||
|
||||
const logData = fs.readFileSync(logFile, 'utf8');
|
||||
const logLines = logData.trim().split('\n').slice(-parseInt(lines));
|
||||
|
||||
const logs = logLines.map(line => {
|
||||
try {
|
||||
return JSON.parse(line);
|
||||
} catch (e) {
|
||||
return { message: line, timestamp: new Date().toISOString(), level: 'info' };
|
||||
}
|
||||
}).reverse(); // Новые записи сверху
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
logs,
|
||||
total: logs.length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
337
backend/src/routes/operators.js
Normal file
337
backend/src/routes/operators.js
Normal file
@@ -0,0 +1,337 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* Middleware для проверки аутентификации оператора
|
||||
*/
|
||||
const authenticateOperator = (req, res, next) => {
|
||||
const operatorId = req.headers['x-operator-id'];
|
||||
|
||||
if (!operatorId) {
|
||||
return res.status(401).json({ error: 'Operator ID required' });
|
||||
}
|
||||
|
||||
const operator = req.app.locals.deviceManager.getOperator(operatorId);
|
||||
if (!operator || !operator.isConnected()) {
|
||||
return res.status(401).json({ error: 'Invalid or disconnected operator' });
|
||||
}
|
||||
|
||||
req.operator = operator;
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* Middleware для проверки разрешений
|
||||
*/
|
||||
const requirePermission = (permission) => {
|
||||
return (req, res, next) => {
|
||||
if (!req.operator.hasPermission(permission)) {
|
||||
return res.status(403).json({ error: `Permission '${permission}' required` });
|
||||
}
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* GET /api/operators/devices
|
||||
* Получение списка доступных устройств
|
||||
*/
|
||||
router.get('/devices', authenticateOperator, requirePermission('view_cameras'), (req, res) => {
|
||||
try {
|
||||
const { deviceManager } = req.app.locals;
|
||||
const devices = deviceManager.getAvailableDevicesForOperator(req.operator.operatorId);
|
||||
|
||||
const devicesData = devices.map(device => device.getSummary());
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
devices: devicesData,
|
||||
total: devicesData.length
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/operators/devices/:deviceId
|
||||
* Получение информации о конкретном устройстве
|
||||
*/
|
||||
router.get('/devices/:deviceId', authenticateOperator, requirePermission('view_cameras'), (req, res) => {
|
||||
try {
|
||||
const { deviceManager } = req.app.locals;
|
||||
const device = deviceManager.getDevice(req.params.deviceId);
|
||||
|
||||
if (!device) {
|
||||
return res.status(404).json({ error: 'Device not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
device: device.getSummary()
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/operators/camera/request
|
||||
* Создание запроса на доступ к камере
|
||||
*/
|
||||
router.post('/camera/request', authenticateOperator, requirePermission('request_camera'), (req, res) => {
|
||||
try {
|
||||
const { deviceId, cameraType = 'back' } = req.body;
|
||||
|
||||
if (!deviceId) {
|
||||
return res.status(400).json({ error: 'Device ID required' });
|
||||
}
|
||||
|
||||
const { deviceManager, sessionManager, io } = req.app.locals;
|
||||
const device = deviceManager.getDevice(deviceId);
|
||||
|
||||
if (!device) {
|
||||
return res.status(404).json({ error: 'Device not found' });
|
||||
}
|
||||
|
||||
if (!device.canAcceptNewSession()) {
|
||||
return res.status(409).json({ error: 'Device is busy or unavailable' });
|
||||
}
|
||||
|
||||
// Создаем сессию
|
||||
const session = sessionManager.createSession(deviceId, req.operator.operatorId, cameraType);
|
||||
|
||||
// Отправляем запрос на Android устройство
|
||||
device.socket.emit('camera:request', {
|
||||
sessionId: session.sessionId,
|
||||
operatorId: req.operator.operatorId,
|
||||
cameraType
|
||||
});
|
||||
|
||||
// Добавляем сессию к устройству и оператору
|
||||
device.addSession(session.sessionId);
|
||||
req.operator.addSession(session.sessionId);
|
||||
|
||||
req.app.locals.logger.info(`Camera request created: ${session.sessionId}`, {
|
||||
deviceId,
|
||||
operatorId: req.operator.operatorId,
|
||||
cameraType
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
sessionId: session.sessionId,
|
||||
message: 'Camera request sent to device'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
req.app.locals.logger.error('Error creating camera request', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/operators/camera/:sessionId/switch
|
||||
* Переключение типа камеры в активной сессии
|
||||
*/
|
||||
router.post('/camera/:sessionId/switch', authenticateOperator, requirePermission('request_camera'), (req, res) => {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
const { cameraType } = req.body;
|
||||
|
||||
if (!cameraType) {
|
||||
return res.status(400).json({ error: 'Camera type required' });
|
||||
}
|
||||
|
||||
const { sessionManager, deviceManager, io } = req.app.locals;
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
|
||||
if (!session) {
|
||||
return res.status(404).json({ error: 'Session not found' });
|
||||
}
|
||||
|
||||
if (session.operatorId !== req.operator.operatorId) {
|
||||
return res.status(403).json({ error: 'Access denied to this session' });
|
||||
}
|
||||
|
||||
if (session.status !== 'active') {
|
||||
return res.status(409).json({ error: 'Session is not active' });
|
||||
}
|
||||
|
||||
const device = deviceManager.getDevice(session.deviceId);
|
||||
if (!device || !device.isConnected()) {
|
||||
return res.status(409).json({ error: 'Device not available' });
|
||||
}
|
||||
|
||||
// Отправляем команду переключения камеры
|
||||
device.socket.emit('camera:switch', {
|
||||
sessionId,
|
||||
cameraType
|
||||
});
|
||||
|
||||
// Обновляем сессию
|
||||
session.switchCamera(cameraType);
|
||||
|
||||
req.app.locals.logger.info(`Camera switch requested: ${sessionId} -> ${cameraType}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Camera switch to ${cameraType} requested`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
req.app.locals.logger.error('Error switching camera', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/operators/camera/:sessionId
|
||||
* Завершение сессии камеры
|
||||
*/
|
||||
router.delete('/camera/:sessionId', authenticateOperator, requirePermission('request_camera'), (req, res) => {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
|
||||
const { sessionManager, deviceManager } = req.app.locals;
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
|
||||
if (!session) {
|
||||
return res.status(404).json({ error: 'Session not found' });
|
||||
}
|
||||
|
||||
if (session.operatorId !== req.operator.operatorId) {
|
||||
return res.status(403).json({ error: 'Access denied to this session' });
|
||||
}
|
||||
|
||||
const device = deviceManager.getDevice(session.deviceId);
|
||||
|
||||
// Отправляем команду отключения (если устройство подключено)
|
||||
if (device && device.isConnected()) {
|
||||
device.socket.emit('camera:disconnect', { sessionId });
|
||||
device.removeSession(sessionId);
|
||||
}
|
||||
|
||||
// Закрываем сессию
|
||||
sessionManager.closeSession(sessionId);
|
||||
req.operator.removeSession(sessionId);
|
||||
|
||||
req.app.locals.logger.info(`Session terminated: ${sessionId}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Session terminated'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
req.app.locals.logger.error('Error terminating session', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/operators/sessions
|
||||
* Получение активных сессий оператора
|
||||
*/
|
||||
router.get('/sessions', authenticateOperator, (req, res) => {
|
||||
try {
|
||||
const { sessionManager } = req.app.locals;
|
||||
const sessions = sessionManager.getOperatorSessions(req.operator.operatorId);
|
||||
|
||||
const sessionsData = sessions.map(session => session.getSummary());
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
sessions: sessionsData,
|
||||
total: sessionsData.length
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/operators/sessions/:sessionId
|
||||
* Получение детальной информации о сессии
|
||||
*/
|
||||
router.get('/sessions/:sessionId', authenticateOperator, (req, res) => {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
const { sessionManager } = req.app.locals;
|
||||
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
if (!session) {
|
||||
return res.status(404).json({ error: 'Session not found' });
|
||||
}
|
||||
|
||||
if (session.operatorId !== req.operator.operatorId && !req.operator.hasPermission('admin')) {
|
||||
return res.status(403).json({ error: 'Access denied to this session' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
session: session.getFullInfo()
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/operators/profile
|
||||
* Получение профиля текущего оператора
|
||||
*/
|
||||
router.get('/profile', authenticateOperator, (req, res) => {
|
||||
try {
|
||||
res.json({
|
||||
success: true,
|
||||
operator: req.operator.getSummary()
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/operators/ping/:deviceId
|
||||
* Проверка доступности устройства
|
||||
*/
|
||||
router.post('/ping/:deviceId', authenticateOperator, requirePermission('view_cameras'), (req, res) => {
|
||||
try {
|
||||
const { deviceId } = req.params;
|
||||
const { deviceManager } = req.app.locals;
|
||||
|
||||
const device = deviceManager.getDevice(deviceId);
|
||||
if (!device || !device.isConnected()) {
|
||||
return res.status(404).json({ error: 'Device not found or disconnected' });
|
||||
}
|
||||
|
||||
const pingStart = Date.now();
|
||||
|
||||
// Отправляем ping и ждем pong
|
||||
device.socket.emit('ping', { timestamp: pingStart }, (response) => {
|
||||
const responseTime = Date.now() - pingStart;
|
||||
device.updateHealthCheck(responseTime);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
deviceId,
|
||||
responseTime,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// Таймаут для ping
|
||||
setTimeout(() => {
|
||||
if (!res.headersSent) {
|
||||
device.recordError();
|
||||
res.status(408).json({ error: 'Device ping timeout' });
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
381
backend/src/server.js
Normal file
381
backend/src/server.js
Normal file
@@ -0,0 +1,381 @@
|
||||
const express = require('express');
|
||||
const http = require('http');
|
||||
const socketIo = require('socket.io');
|
||||
const cors = require('cors');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const winston = require('winston');
|
||||
|
||||
// Импорт наших менеджеров
|
||||
const { SessionManager } = require('./managers/SessionManager');
|
||||
const { DeviceManager } = require('./managers/DeviceManager');
|
||||
|
||||
// Импорт роутов
|
||||
const operatorsRouter = require('./routes/operators');
|
||||
const adminRouter = require('./routes/admin');
|
||||
|
||||
// Настройка логгера
|
||||
const logger = winston.createLogger({
|
||||
level: 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.json()
|
||||
),
|
||||
transports: [
|
||||
new winston.transports.Console(),
|
||||
new winston.transports.File({ filename: 'god-eye.log' })
|
||||
]
|
||||
});
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const io = socketIo(server, {
|
||||
cors: {
|
||||
origin: "*",
|
||||
methods: ["GET", "POST"]
|
||||
}
|
||||
});
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.static('public')); // Для статических файлов демо
|
||||
|
||||
// Инициализация менеджеров
|
||||
const sessionManager = new SessionManager();
|
||||
const deviceManager = new DeviceManager();
|
||||
|
||||
// Делаем менеджеры доступными в роутах
|
||||
app.locals.sessionManager = sessionManager;
|
||||
app.locals.deviceManager = deviceManager;
|
||||
app.locals.logger = logger;
|
||||
app.locals.io = io;
|
||||
|
||||
// Подключаем роуты
|
||||
app.use('/api/operators', operatorsRouter);
|
||||
app.use('/api/admin', adminRouter);
|
||||
|
||||
// Основные REST API маршруты для мониторинга
|
||||
app.get('/api/status', (req, res) => {
|
||||
const deviceStats = deviceManager.getStats();
|
||||
const sessionStats = sessionManager.getStats();
|
||||
|
||||
res.json({
|
||||
status: 'online',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: Math.round(process.uptime()),
|
||||
devices: deviceStats,
|
||||
sessions: sessionStats
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/devices', (req, res) => {
|
||||
const devices = deviceManager.getConnectedDevices();
|
||||
const devicesData = devices.map(device => device.getSummary());
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
devices: devicesData,
|
||||
total: devicesData.length
|
||||
});
|
||||
});
|
||||
|
||||
// WebSocket обработчики
|
||||
io.on('connection', (socket) => {
|
||||
logger.info(`New connection: ${socket.id}`);
|
||||
|
||||
// Регистрация Android клиента
|
||||
socket.on('register:android', (data) => {
|
||||
const { deviceId, deviceInfo } = data;
|
||||
|
||||
// Регистрируем устройство через DeviceManager
|
||||
const device = deviceManager.registerDevice(deviceId, deviceInfo, socket);
|
||||
|
||||
logger.info(`Android client registered: ${deviceId}`, deviceInfo);
|
||||
|
||||
// Уведомляем всех операторов о новом устройстве
|
||||
const operatorSockets = Array.from(deviceManager.operators.values())
|
||||
.filter(op => op.isConnected())
|
||||
.map(op => op.socket);
|
||||
|
||||
operatorSockets.forEach(opSocket => {
|
||||
opSocket.emit('device:connected', {
|
||||
deviceId,
|
||||
deviceInfo: device.getSummary()
|
||||
});
|
||||
});
|
||||
|
||||
socket.emit('register:success', { deviceId });
|
||||
});
|
||||
|
||||
// Регистрация оператора
|
||||
socket.on('register:operator', (data) => {
|
||||
const { operatorId, operatorInfo } = data;
|
||||
const finalOperatorId = operatorId || uuidv4();
|
||||
|
||||
// Регистрируем оператора через DeviceManager
|
||||
const operator = deviceManager.registerOperator(finalOperatorId, operatorInfo, socket);
|
||||
|
||||
logger.info(`Operator registered: ${finalOperatorId}`);
|
||||
|
||||
// Отправляем список доступных устройств
|
||||
const availableDevices = deviceManager.getAvailableDevicesForOperator(finalOperatorId);
|
||||
const devicesData = availableDevices.map(device => device.getSummary());
|
||||
|
||||
socket.emit('register:success', {
|
||||
operatorId: finalOperatorId,
|
||||
availableDevices: devicesData
|
||||
});
|
||||
});
|
||||
|
||||
// Запрос на подключение к камере (только через WebSocket для обратной совместимости)
|
||||
socket.on('camera:request', (data) => {
|
||||
const { deviceId, cameraType = 'back' } = data;
|
||||
|
||||
// Получаем оператора из менеджера устройств
|
||||
const operator = Array.from(deviceManager.operators.values())
|
||||
.find(op => op.socket === socket);
|
||||
|
||||
if (!operator) {
|
||||
socket.emit('camera:error', { error: 'Operator not registered' });
|
||||
return;
|
||||
}
|
||||
|
||||
const device = deviceManager.getDevice(deviceId);
|
||||
if (!device || !device.canAcceptNewSession()) {
|
||||
socket.emit('camera:error', { error: 'Device not available' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Создаем сессию
|
||||
const session = sessionManager.createSession(deviceId, operator.operatorId, cameraType);
|
||||
|
||||
logger.info(`Camera request: operator ${operator.operatorId} -> device ${deviceId}`);
|
||||
|
||||
// Отправляем запрос Android клиенту
|
||||
device.socket.emit('camera:request', {
|
||||
sessionId: session.sessionId,
|
||||
operatorId: operator.operatorId,
|
||||
cameraType
|
||||
});
|
||||
|
||||
// Добавляем сессию к участникам
|
||||
device.addSession(session.sessionId);
|
||||
operator.addSession(session.sessionId);
|
||||
|
||||
socket.emit('camera:request-sent', { sessionId: session.sessionId, deviceId });
|
||||
});
|
||||
|
||||
// Ответ от Android клиента на запрос камеры
|
||||
socket.on('camera:response', (data) => {
|
||||
const { sessionId, accepted, streamUrl, error } = data;
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
|
||||
if (!session) {
|
||||
socket.emit('camera:error', { error: 'Session not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const operator = deviceManager.getOperator(session.operatorId);
|
||||
if (!operator || !operator.isConnected()) {
|
||||
socket.emit('camera:error', { error: 'Operator not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (accepted) {
|
||||
session.updateStatus('active', { streamUrl });
|
||||
logger.info(`Camera stream started: session ${sessionId}`);
|
||||
operator.socket.emit('camera:stream-ready', { sessionId, streamUrl });
|
||||
} else {
|
||||
session.updateStatus('denied', { error });
|
||||
logger.info(`Camera request denied: session ${sessionId}`, error);
|
||||
operator.socket.emit('camera:denied', { sessionId, error });
|
||||
|
||||
// Очищаем отклоненную сессию
|
||||
const device = deviceManager.getDevice(session.deviceId);
|
||||
if (device) device.removeSession(sessionId);
|
||||
operator.removeSession(sessionId);
|
||||
sessionManager.closeSession(sessionId);
|
||||
}
|
||||
});
|
||||
|
||||
// WebRTC сигнализация
|
||||
socket.on('webrtc:offer', (data) => {
|
||||
const { sessionId, offer } = data;
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
|
||||
if (session) {
|
||||
// Определяем получателя (Android -> Operator или Operator -> Android)
|
||||
const device = deviceManager.getDevice(session.deviceId);
|
||||
const operator = deviceManager.getOperator(session.operatorId);
|
||||
|
||||
// Если отправитель - устройство, то получатель - оператор
|
||||
if (device && device.socket === socket && operator && operator.isConnected()) {
|
||||
operator.socket.emit('webrtc:offer', { sessionId, offer });
|
||||
session.updateWebRTCState('offer_sent');
|
||||
}
|
||||
// Если отправитель - оператор, то получатель - устройство
|
||||
else if (operator && operator.socket === socket && device && device.isConnected()) {
|
||||
device.socket.emit('webrtc:offer', { sessionId, offer });
|
||||
session.updateWebRTCState('offer_sent');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('webrtc:answer', (data) => {
|
||||
const { sessionId, answer } = data;
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
|
||||
if (session) {
|
||||
const device = deviceManager.getDevice(session.deviceId);
|
||||
const operator = deviceManager.getOperator(session.operatorId);
|
||||
|
||||
// Если отправитель - устройство, то получатель - оператор
|
||||
if (device && device.socket === socket && operator && operator.isConnected()) {
|
||||
operator.socket.emit('webrtc:answer', { sessionId, answer });
|
||||
session.updateWebRTCState('answer_sent');
|
||||
}
|
||||
// Если отправитель - оператор, то получатель - устройство
|
||||
else if (operator && operator.socket === socket && device && device.isConnected()) {
|
||||
device.socket.emit('webrtc:answer', { sessionId, answer });
|
||||
session.updateWebRTCState('answer_sent');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('webrtc:ice-candidate', (data) => {
|
||||
const { sessionId, candidate } = data;
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
|
||||
if (session) {
|
||||
const device = deviceManager.getDevice(session.deviceId);
|
||||
const operator = deviceManager.getOperator(session.operatorId);
|
||||
|
||||
// Пересылаем ICE кандидата другой стороне
|
||||
if (device && device.socket === socket && operator && operator.isConnected()) {
|
||||
operator.socket.emit('webrtc:ice-candidate', { sessionId, candidate });
|
||||
} else if (operator && operator.socket === socket && device && device.isConnected()) {
|
||||
device.socket.emit('webrtc:ice-candidate', { sessionId, candidate });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Переключение типа камеры
|
||||
socket.on('camera:switch', (data) => {
|
||||
const { sessionId, cameraType } = data;
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
|
||||
if (session) {
|
||||
const device = deviceManager.getDevice(session.deviceId);
|
||||
const operator = deviceManager.getOperator(session.operatorId);
|
||||
|
||||
// Проверяем права доступа
|
||||
if (operator && operator.socket === socket && device && device.isConnected()) {
|
||||
device.socket.emit('camera:switch', { sessionId, cameraType });
|
||||
session.switchCamera(cameraType);
|
||||
logger.info(`Camera switch requested: ${sessionId} -> ${cameraType}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Завершение сессии
|
||||
socket.on('camera:disconnect', (data) => {
|
||||
const { sessionId } = data;
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
|
||||
if (session) {
|
||||
const device = deviceManager.getDevice(session.deviceId);
|
||||
const operator = deviceManager.getOperator(session.operatorId);
|
||||
|
||||
// Уведомляем участников
|
||||
if (device && device.isConnected()) {
|
||||
device.socket.emit('camera:disconnect', { sessionId });
|
||||
device.removeSession(sessionId);
|
||||
}
|
||||
|
||||
if (operator && operator.isConnected()) {
|
||||
operator.socket.emit('camera:disconnected', { sessionId });
|
||||
operator.removeSession(sessionId);
|
||||
}
|
||||
|
||||
sessionManager.closeSession(sessionId);
|
||||
logger.info(`Camera session ended: ${sessionId}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Ping-Pong для проверки соединения
|
||||
socket.on('ping', (data, callback) => {
|
||||
if (callback) {
|
||||
callback({ timestamp: Date.now(), ...data });
|
||||
}
|
||||
});
|
||||
|
||||
// Обработка отключения
|
||||
socket.on('disconnect', (reason) => {
|
||||
logger.info(`Client disconnected: ${socket.id}, reason: ${reason}`);
|
||||
|
||||
// Находим устройство или оператора по сокету
|
||||
const device = Array.from(deviceManager.devices.values())
|
||||
.find(d => d.socket === socket);
|
||||
|
||||
const operator = Array.from(deviceManager.operators.values())
|
||||
.find(op => op.socket === socket);
|
||||
|
||||
if (device) {
|
||||
// Уведомляем операторов об отключении устройства
|
||||
const operators = deviceManager.getConnectedOperators();
|
||||
operators.forEach(op => {
|
||||
op.socket.emit('device:disconnected', {
|
||||
deviceId: device.deviceId
|
||||
});
|
||||
});
|
||||
|
||||
// Завершаем активные сессии устройства
|
||||
sessionManager.closeDeviceSessions(device.deviceId);
|
||||
deviceManager.disconnectDevice(device.deviceId);
|
||||
}
|
||||
|
||||
if (operator) {
|
||||
// Завершаем активные сессии оператора
|
||||
sessionManager.closeOperatorSessions(operator.operatorId);
|
||||
deviceManager.disconnectOperator(operator.operatorId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Периодическая очистка старых сессий и устройств
|
||||
setInterval(() => {
|
||||
try {
|
||||
sessionManager.cleanupOldSessions(60); // Очистка сессий старше 60 минут
|
||||
deviceManager.cleanup(); // Очистка отключенных устройств
|
||||
} catch (error) {
|
||||
logger.error('Error during periodic cleanup', error);
|
||||
}
|
||||
}, 5 * 60 * 1000); // Каждые 5 минут
|
||||
|
||||
// Обработка graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
logger.info('SIGTERM received, shutting down gracefully');
|
||||
server.close(() => {
|
||||
logger.info('Process terminated');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
logger.info('SIGINT received, shutting down gracefully');
|
||||
server.close(() => {
|
||||
logger.info('Process terminated');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
server.listen(PORT, () => {
|
||||
logger.info(`GodEye Backend Server running on port ${PORT}`);
|
||||
console.log(`🚀 Server started on http://localhost:${PORT}`);
|
||||
console.log(`📊 Admin API: http://localhost:${PORT}/api/admin/stats`);
|
||||
console.log(`👥 Operators API: http://localhost:${PORT}/api/operators/devices`);
|
||||
console.log(`📱 Status: http://localhost:${PORT}/api/status`);
|
||||
console.log(`🌐 Demo: http://localhost:${PORT}/`);
|
||||
});
|
||||
Reference in New Issue
Block a user