main commit

This commit is contained in:
2025-10-04 11:55:55 +09:00
parent c8c3274527
commit 4ceccae6ce
678 changed files with 95975 additions and 185 deletions

View File

@@ -6,7 +6,8 @@ class GodEyeOperator {
constructor() {
this.socket = null;
this.operatorId = uuidv4();
this.currentSession = null;
this.activeSessions = new Map(); // sessionId -> sessionData
this.currentActiveSession = null; // Currently viewing session
this.localConnection = null;
this.remoteStream = null;
this.mediaRecorder = null;
@@ -18,6 +19,7 @@ class GodEyeOperator {
this.isFullscreen = false;
this.isZoomed = false;
this.config = null;
this.isConnected = false;
// UI Elements
this.elements = {
@@ -152,6 +154,56 @@ class GodEyeOperator {
this.elements.connectBtn.addEventListener('click', () => this.connect());
this.elements.disconnectBtn.addEventListener('click', () => this.disconnect());
this.elements.clearLogs.addEventListener('click', () => this.clearLogs());
// Logs panel toggle
const logsToggle = document.getElementById('logs-toggle');
const logsContent = document.getElementById('logs-content');
const logsCollapseIcon = logsToggle?.querySelector('.collapse-icon');
const controlPanel = document.querySelector('.control-panel');
if (logsToggle) {
logsToggle.addEventListener('click', () => {
const isCollapsed = logsContent.classList.contains('collapsed');
if (isCollapsed) {
// Разворачиваем
logsContent.classList.remove('collapsed');
logsCollapseIcon.classList.remove('collapsed');
controlPanel.classList.remove('logs-collapsed');
logsCollapseIcon.textContent = '▲';
} else {
// Сворачиваем
logsContent.classList.add('collapsed');
logsCollapseIcon.classList.add('collapsed');
controlPanel.classList.add('logs-collapsed');
logsCollapseIcon.textContent = '▼';
}
});
}
// Connection panel toggle
const connectionToggle = document.getElementById('connection-toggle');
const connectionContent = document.getElementById('connection-content');
const connectionCollapseIcon = connectionToggle?.querySelector('.collapse-icon');
if (connectionToggle) {
connectionToggle.addEventListener('click', () => {
const isCollapsed = connectionContent.classList.contains('collapsed');
if (isCollapsed) {
// Разворачиваем
connectionContent.classList.remove('collapsed');
connectionCollapseIcon.classList.remove('collapsed');
connectionCollapseIcon.textContent = '▲';
} else {
// Сворачиваем
connectionContent.classList.add('collapsed');
connectionCollapseIcon.classList.add('collapsed');
connectionCollapseIcon.textContent = '▼';
}
});
}
// Refresh devices
const refreshBtn = document.getElementById('refresh-devices');
if (refreshBtn) {
@@ -288,6 +340,11 @@ class GodEyeOperator {
}
connect() {
if (this.isConnected) {
this.disconnect();
return;
}
const serverUrl = this.elements.serverUrl.value.trim();
if (!serverUrl) {
this.log('Введите URL сервера', 'error');
@@ -321,7 +378,7 @@ class GodEyeOperator {
this.log('Отключен от сервера', 'warning');
this.updateConnectionStatus(false);
this.clearDevicesList();
this.clearSessionsList();
this.updateSessionsList();
this.stopPingMonitoring();
});
@@ -381,6 +438,133 @@ class GodEyeOperator {
this.elements.pingIndicator.textContent = `Ping: ${ping}ms`;
this.elements.pingIndicator.style.color = ping < 100 ? '#4CAF50' : ping < 300 ? '#FF9800' : '#f44336';
});
// Session events
this.socket.on('session:created', (data) => {
this.log(`Сессия создана: ${data.sessionId}`, 'info');
this.activeSessions.set(data.sessionId, {
...data,
status: 'pending'
});
this.updateSessionsList();
});
// Новые события ConnectionManager
this.socket.on('connection:initiated', (data) => {
this.log(`Подключение инициировано: ${data.connectionId}`, 'info');
this.activeSessions.set(data.sessionId, {
connectionId: data.connectionId,
sessionId: data.sessionId,
deviceId: data.deviceId,
cameraType: data.cameraType,
status: 'pending',
createdAt: data.createdAt
});
this.updateSessionsList();
});
this.socket.on('connection:accepted', (data) => {
this.log(`Подключение принято: ${data.connectionId}`, 'success');
const session = this.activeSessions.get(data.sessionId);
if (session) {
session.status = 'active';
session.streamUrl = data.streamUrl;
if (!this.currentActiveSession) {
this.currentActiveSession = data.sessionId;
this.currentSession = {
id: data.sessionId,
deviceId: data.deviceId,
cameraType: data.cameraType
};
this.elements.sessionInfo.textContent = `Активная сессия: ${session.deviceId} (${session.cameraType})`;
this.updateCameraButtons(session.cameraType);
this.initWebRTC();
}
this.updateSessionsList();
}
});
this.socket.on('connection:rejected', (data) => {
this.log(`Подключение отклонено: ${data.sessionId} - ${data.error}`, 'error');
const session = this.activeSessions.get(data.sessionId);
if (session) {
session.status = 'rejected';
session.error = data.error;
this.updateSessionsList();
}
});
this.socket.on('connection:terminated', (data) => {
this.log(`Подключение завершено: ${data.connectionId}`, 'info');
// Находим сессию по connectionId
for (const [sessionId, session] of this.activeSessions.entries()) {
if (session.connectionId === data.connectionId) {
this.activeSessions.delete(sessionId);
if (this.currentActiveSession === sessionId) {
this.currentActiveSession = null;
this.currentSession = null;
this.elements.sessionInfo.textContent = '';
this.closeWebRTC();
}
break;
}
}
this.updateSessionsList();
});
this.socket.on('connection:error', (data) => {
this.log(`Ошибка подключения: ${data.error}`, 'error');
});
// Сохраняем старые события для обратной совместимости
this.socket.on('session:created', (data) => {
this.log(`Сессия создана: ${data.sessionId}`, 'info');
this.activeSessions.set(data.sessionId, {
...data,
status: 'pending'
});
this.updateSessionsList();
});
this.socket.on('session:rejected', (data) => {
this.log(`Сессия отклонена: ${data.sessionId}`, 'error');
const session = this.activeSessions.get(data.sessionId);
if (session) {
session.status = 'rejected';
this.updateSessionsList();
}
});
this.socket.on('session:ended', (data) => {
this.log(`Сессия завершена: ${data.sessionId}`, 'info');
this.activeSessions.delete(data.sessionId);
if (this.currentActiveSession === data.sessionId) {
this.currentActiveSession = null;
this.elements.sessionInfo.textContent = '';
}
this.updateSessionsList();
});
// Дополнительный обработчик для совместимости
this.socket.on('camera:disconnected', (data) => {
this.log(`Камера отключена: ${data.sessionId}`, 'warning');
const session = this.activeSessions.get(data.sessionId);
if (session) {
session.status = 'ended';
this.updateSessionsList();
}
});
// Обработчик обновления списка устройств
this.socket.on('device:connected', (data) => {
this.log(`Новое устройство подключено: ${data.deviceId}`, 'info');
this.requestDevicesList(); // Обновляем список устройств
});
this.socket.on('device:disconnected', (data) => {
this.log(`Устройство отключено: ${data.deviceId}`, 'warning');
this.requestDevicesList(); // Обновляем список устройств
});
}
registerOperator() {
@@ -404,18 +588,46 @@ class GodEyeOperator {
}
updateConnectionStatus(connected) {
this.elements.connectBtn.disabled = connected;
this.elements.disconnectBtn.disabled = !connected;
this.isConnected = connected;
// Обновляем заголовок панели подключения
const connectionToggle = document.getElementById('connection-toggle');
const connectionTitle = connectionToggle?.querySelector('h3');
if (connected) {
this.elements.connectBtn.textContent = 'Отключиться';
this.elements.connectBtn.className = 'btn-danger';
this.elements.connectBtn.disabled = false;
this.elements.disconnectBtn.disabled = false;
this.elements.connectionIndicator.textContent = '● Подключен';
this.elements.connectionIndicator.className = 'status connected';
this.elements.connectionStatusText.textContent = 'Подключен к серверу';
// Обновляем заголовок с индикатором подключения
if (connectionTitle) {
connectionTitle.innerHTML = '🔗 Подключение к серверу <span style="color: #4CAF50;">●</span>';
}
} else {
this.elements.connectBtn.textContent = 'Подключиться';
this.elements.connectBtn.className = 'btn-primary';
this.elements.connectBtn.disabled = false;
this.elements.disconnectBtn.disabled = true;
this.elements.connectionIndicator.textContent = '● Отключен';
this.elements.connectionIndicator.className = 'status disconnected';
this.elements.connectionStatusText.textContent = 'Не подключен';
this.elements.sessionInfo.textContent = '';
// Обновляем заголовок с индикатором отключения
if (connectionTitle) {
connectionTitle.innerHTML = '🔗 Подключение к серверу <span style="color: #f44336;">●</span>';
}
// Очищаем сессии при отключении
if (this.activeSessions) {
this.activeSessions.clear();
this.currentActiveSession = null;
this.updateSessionsList();
}
}
}
@@ -431,17 +643,31 @@ class GodEyeOperator {
devices.forEach(device => {
const deviceElement = document.createElement('div');
deviceElement.className = 'device-item';
// Проверяем, есть ли активные сессии с этим устройством
const activeSessions = Array.from(this.activeSessions.values())
.filter(session => session.deviceId === device.deviceId && session.status === 'active');
const hasActiveSessions = activeSessions.length > 0;
// Сокращенный ID для компактности
const shortId = device.deviceId.length > 12 ?
device.deviceId.substring(0, 8) + '...' : device.deviceId;
deviceElement.innerHTML = `
<div class="device-info">
<strong>ID:</strong> ${device.deviceId}<br>
<strong>Статус:</strong> ${device.isConnected ? 'Онлайн' : 'Офлайн'}
<strong>ID:</strong> ${shortId}<br>
<strong>Статус:</strong> ${device.isConnected ? '🟢 Онлайн' : '🔴 Офлайн'}
${hasActiveSessions ? `<br><strong>Сессии:</strong> ${activeSessions.length}` : ''}
</div>
<div class="device-capabilities">
Камеры: ${(Array.isArray(device.capabilities) ? device.capabilities.join(', ') : (device.capabilities?.cameras?.join(', ') || ''))}
📷 ${(Array.isArray(device.capabilities) ? device.capabilities.join(', ') : (device.capabilities?.cameras?.join(', ') || 'back'))}
</div>
<div class="device-actions">
<button class="btn-device" onclick="operator.requestCamera('${device.deviceId}', 'back')">
Подключиться
<button class="btn-device ${hasActiveSessions ? 'btn-success' : 'btn-primary'}"
onclick="operator.requestCamera('${device.deviceId}', 'back')"
title="${hasActiveSessions ? 'Добавить новую сессию' : 'Подключиться к устройству'}">
${hasActiveSessions ? ' Добавить' : '🔗 Подключить'}
</button>
</div>
`;
@@ -454,8 +680,129 @@ class GodEyeOperator {
this.elements.devicesList.innerHTML = '<div class="no-devices">Нет подключенных устройств</div>';
}
clearSessionsList() {
this.elements.sessionsList.innerHTML = '<div class="no-sessions">Нет активных сессий</div>';
updateSessionsList() {
const container = this.elements.sessionsList;
if (this.activeSessions.size === 0) {
container.innerHTML = '<div class="no-sessions">Нет активных сессий</div>';
return;
}
container.innerHTML = '';
this.activeSessions.forEach((session, sessionId) => {
const sessionElement = document.createElement('div');
sessionElement.className = `session-item session-${session.status} ${this.currentActiveSession === sessionId ? 'active' : ''}`;
const statusText = {
'pending': '🟠 Ожидание',
'active': '🟢 Активна',
'rejected': '🔴 Отклонена',
'ended': '⚫ Завершена'
};
// Сокращенный ID устройства
const shortDeviceId = session.deviceId.length > 10 ?
session.deviceId.substring(0, 8) + '...' : session.deviceId;
sessionElement.innerHTML = `
<div class="session-header">
<strong>📱 ${shortDeviceId}</strong> | <strong>📷 ${session.cameraType}</strong><br>
<span class="status-${session.status}">${statusText[session.status] || session.status}</span>
</div>
<div class="session-actions">
${session.status === 'active' ? `
<button class="btn-small ${this.currentActiveSession === sessionId ? 'btn-success' : 'btn-primary'}"
onclick="operator.switchToSession('${sessionId}')"
title="${this.currentActiveSession === sessionId ? 'Активная сессия' : 'Переключиться на эту сессию'}">
${this.currentActiveSession === sessionId ? '✓ Активна' : '🔄 Переключить'}
</button>
<button class="btn-small btn-secondary" onclick="operator.switchCamera('${sessionId}', 'front')"
title="Переключить на фронтальную камеру">
📷
</button>
${session.connectionId ? `
<button class="btn-small btn-warning" onclick="operator.terminateConnection('${session.connectionId}', '${sessionId}')"
title="Завершить подключение">
🔌❌
</button>
` : `
<button class="btn-small btn-danger" onclick="operator.endSession('${sessionId}')"
title="Завершить сессию">
</button>
`}
` : `
<button class="btn-small btn-secondary" onclick="operator.endSession('${sessionId}')"
title="Удалить из списка">
🗑️
</button>
`}
</div>
`;
container.appendChild(sessionElement);
});
}
switchToSession(sessionId) {
const session = this.activeSessions.get(sessionId);
if (!session || session.status !== 'active') {
this.log(`Нельзя переключиться на сессию ${sessionId}`, 'error');
return;
}
// Если это уже активная сессия, ничего не делаем
if (this.currentActiveSession === sessionId) {
this.log('Эта сессия уже активна', 'info');
return;
}
this.currentActiveSession = sessionId;
// Обновляем currentSession для обратной совместимости
this.currentSession = {
id: sessionId,
deviceId: session.deviceId,
cameraType: session.cameraType
};
this.log(`Переключение на сессию: ${sessionId} (устройство: ${session.deviceId})`, 'info');
this.elements.sessionInfo.textContent = `Активная сессия: ${session.deviceId} (${session.cameraType})`;
// Обновляем кнопки камеры
this.updateCameraButtons(session.cameraType);
// Обновляем список сессий
this.updateSessionsList();
// TODO: Переключить видеопоток
// В будущем здесь будет логика переключения WebRTC потоков
}
switchCamera(sessionId, cameraType) {
const session = this.activeSessions.get(sessionId);
if (!session || session.status !== 'active') {
this.log(`Нельзя переключить камеру в сессии ${sessionId}`, 'error');
return;
}
this.log(`Переключение камеры в сессии ${sessionId} на ${cameraType}`, 'info');
if (this.socket) {
this.socket.emit('camera:switch', {
sessionId: sessionId,
cameraType: cameraType
});
}
// Обновляем информацию о сессии
session.cameraType = cameraType;
this.updateSessionsList();
if (this.currentActiveSession === sessionId) {
this.elements.sessionInfo.textContent = `Активная сессия: ${session.deviceId} (${session.cameraType})`;
}
}
requestCamera(deviceId, cameraType = 'back') {
@@ -466,6 +813,7 @@ class GodEyeOperator {
this.log(`Запрос доступа к камере ${cameraType} устройства ${deviceId}`, 'info');
// Используем новое событие для подключения через ConnectionManager
this.socket.emit('camera:request', {
deviceId: deviceId,
operatorId: this.operatorId,
@@ -475,11 +823,27 @@ class GodEyeOperator {
handleCameraResponse(data) {
if (data.success) {
this.currentSession = data.session;
this.elements.sessionInfo.textContent = `Сессия: ${data.session.id}`;
this.log(`Доступ к камере получен. Сессия: ${data.session.id}`, 'success');
this.updateCameraButtons(data.session.cameraType);
this.initWebRTC();
// Обновляем сессию в коллекции
const sessionData = {
sessionId: data.sessionId || data.session.id,
deviceId: data.session.deviceId,
cameraType: data.session.cameraType,
status: 'active'
};
this.activeSessions.set(sessionData.sessionId, sessionData);
// Если нет текущей активной сессии, сделаем эту активной
if (!this.currentActiveSession) {
this.currentActiveSession = sessionData.sessionId;
this.currentSession = data.session; // Сохраняем для обратной совместимости
this.elements.sessionInfo.textContent = `Активная сессия: ${sessionData.deviceId} (${sessionData.cameraType})`;
this.updateCameraButtons(sessionData.cameraType);
this.initWebRTC();
}
this.log(`Доступ к камере получен. Сессия: ${sessionData.sessionId}`, 'success');
this.updateSessionsList();
} else {
this.log(`Отказ в доступе к камере: ${data.message}`, 'error');
}
@@ -494,20 +858,106 @@ class GodEyeOperator {
});
}
switchCamera(cameraType) {
if (!this.currentSession) {
switchCamera(sessionIdOrType, cameraType) {
// Определяем, передан sessionId или это старый вызов
let sessionId, targetCameraType;
if (cameraType) {
// Новый вызов: switchCamera(sessionId, cameraType)
sessionId = sessionIdOrType;
targetCameraType = cameraType;
} else {
// Старый вызов: switchCamera(cameraType)
targetCameraType = sessionIdOrType;
sessionId = this.currentActiveSession;
if (!sessionId && this.currentSession) {
// Обратная совместимость со старым кодом
sessionId = this.currentSession.id;
}
}
if (!sessionId) {
this.log('Нет активной сессии для переключения камеры', 'warning');
return;
}
this.log(`Переключение на камеру: ${cameraType}`, 'info');
const session = this.activeSessions.get(sessionId);
if (!session || session.status !== 'active') {
this.log(`Нельзя переключить камеру в сессии ${sessionId}`, 'error');
return;
}
this.log(`Переключение на камеру: ${targetCameraType} в сессии ${sessionId}`, 'info');
this.socket.emit('camera:switch', {
sessionId: this.currentSession.id,
cameraType: cameraType
sessionId: sessionId,
cameraType: targetCameraType
});
this.updateCameraButtons(cameraType);
// Обновляем состояние кнопок только для активной сессии
if (sessionId === this.currentActiveSession) {
this.updateCameraButtons(targetCameraType);
}
}
// Новые методы для управления подключениями через ConnectionManager
terminateConnection(connectionId, sessionId) {
if (!this.socket || !this.socket.connected) {
this.log('Нет подключения к серверу', 'error');
return;
}
this.log(`Завершение подключения: ${connectionId}`, 'info');
this.socket.emit('connection:terminate', {
connectionId: connectionId
});
// Локально обновляем состояние сессии
const session = this.activeSessions.get(sessionId);
if (session) {
session.status = 'terminating';
this.updateSessionsList();
}
}
getConnectionStatus() {
if (!this.socket || !this.socket.connected) {
this.log('Нет подключения к серверу', 'error');
return;
}
this.socket.emit('connection:status', {}, (response) => {
if (response.success) {
this.log('Статистика подключений получена', 'info');
console.log('Connection Stats:', response.stats);
} else {
this.log(`Ошибка получения статистики: ${response.error}`, 'error');
}
});
}
listMyConnections() {
if (!this.socket || !this.socket.connected) {
this.log('Нет подключения к серверу', 'error');
return;
}
this.socket.emit('connection:list', {}, (response) => {
if (response.success) {
this.log(`Получен список подключений: ${response.connections.length}`, 'info');
console.log('My Connections:', response.connections);
// Можно обновить UI с информацией о подключениях
response.connections.forEach(conn => {
console.log(`Connection ${conn.connectionId}: ${conn.deviceId} -> ${conn.status}`);
});
} else {
this.log(`Ошибка получения списка подключений: ${response.error}`, 'error');
}
});
}
async initWebRTC() {

View File

@@ -100,52 +100,61 @@
<!-- Right Panel - Devices & Sessions -->
<div class="control-panel">
<!-- Connection Settings -->
<div class="connection-panel">
<h3>Подключение к серверу</h3>
<div class="input-group">
<label for="server-url">URL сервера:</label>
<input type="text" id="server-url" value="http://localhost:3001" placeholder="http://localhost:3001">
<div class="connection-panel panel-section collapsible">
<div class="panel-header clickable" id="connection-toggle">
<h3>🔌 Подключение к серверу</h3>
<span class="collapse-icon collapsed"></span>
</div>
<div class="checkbox-group">
<label>
<input type="checkbox" id="auto-connect"> Автоматически подключаться при запуске
</label>
</div>
<div class="button-group">
<button id="connect-btn" class="btn-primary">Подключиться</button>
<button id="disconnect-btn" class="btn-secondary" disabled>Отключиться</button>
</div>
<div id="connection-info" class="connection-info">
<span id="connection-status-text">Не подключен</span>
<span id="ping-indicator">Ping: --</span>
<div class="panel-content collapsed" id="connection-content">
<div class="input-group">
<label for="server-url">URL сервера:</label>
<input type="text" id="server-url" value="http://localhost:3001" placeholder="http://localhost:3001">
</div>
<div class="checkbox-group">
<label>
<input type="checkbox" id="auto-connect"> Автоматически подключаться при запуске
</label>
</div>
<div class="button-group">
<button id="connect-btn" class="btn-primary">Подключиться</button>
<button id="disconnect-btn" class="btn-secondary" disabled>Отключиться</button>
</div>
<div id="connection-info" class="connection-info">
<span id="connection-status-text">Не подключен</span>
<span id="ping-indicator">Ping: --</span>
</div>
</div>
</div>
<!-- Available Devices -->
<div class="devices-panel">
<h3>Доступные устройства</h3>
<div class="devices-header" style="display: flex; justify-content: space-between; align-items: center;">
<span></span>
<button id="refresh-devices" class="btn-secondary btn-small" title="Обновить список устройств">🔄 Обновить</button>
<div class="devices-panel panel-section">
<div class="panel-header">
<h3>📱 Доступные устройства</h3>
<button id="refresh-devices" class="btn-icon" title="Обновить список">🔄</button>
</div>
<div id="devices-list" class="devices-list">
<div id="devices-list" class="devices-list compact-list">
<div class="no-devices">Нет подключенных устройств</div>
</div>
</div>
<!-- Active Sessions -->
<div class="sessions-panel">
<h3>Активные сессии</h3>
<div id="sessions-list" class="sessions-list">
<div class="sessions-panel panel-section">
<h3>🔗 Активные сессии</h3>
<div id="sessions-list" class="sessions-list compact-list">
<div class="no-sessions">Нет активных сессий</div>
</div>
</div>
<!-- Logs -->
<div class="logs-panel">
<h3>Журнал событий</h3>
<div id="logs-container" class="logs-container" style="max-height: 180px; overflow-y: auto;"></div>
<button id="clear-logs" class="btn-secondary btn-small">Очистить</button>
<div class="logs-panel panel-section collapsible">
<div class="panel-header clickable" id="logs-toggle">
<h3>📋 Журнал событий</h3>
<span class="collapse-icon"></span>
</div>
<div class="panel-content" id="logs-content">
<div id="logs-container" class="logs-container"></div>
<button id="clear-logs" class="btn-secondary btn-small">Очистить</button>
</div>
</div>
</div>
</div>

View File

@@ -306,19 +306,91 @@ body {
background: #252525;
display: flex;
flex-direction: column;
max-width: 350px;
max-width: 380px;
min-width: 300px;
transition: max-width 0.3s ease;
}
.control-panel.logs-collapsed {
max-width: 320px;
}
.control-panel > div {
padding: 15px;
border-bottom: 1px solid #333;
}
/* Panel Sections */
.panel-section {
padding: 12px;
transition: all 0.3s ease;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.panel-header.clickable {
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.panel-header.clickable:hover {
background: rgba(255, 255, 255, 0.05);
margin: -5px;
padding: 5px;
border-radius: 4px;
}
.collapse-icon {
font-size: 12px;
transition: transform 0.3s ease;
color: #888;
}
.collapse-icon.collapsed {
transform: rotate(180deg);
}
.panel-content {
transition: all 0.3s ease;
overflow: hidden;
}
.panel-content.collapsed {
max-height: 0;
padding: 0;
opacity: 0;
}
/* Connection Panel */
.connection-panel {
transition: all 0.3s ease;
}
.connection-panel.collapsed {
flex: none;
}
.connection-panel h3 {
margin-bottom: 15px;
margin-bottom: 8px;
color: #4CAF50;
font-size: 14px;
font-size: 13px;
display: flex;
align-items: center;
gap: 6px;
justify-content: space-between;
width: 100%;
}
.connection-panel h3 span {
font-size: 10px;
margin-left: auto;
}
.input-group {
@@ -385,32 +457,53 @@ button:disabled {
cursor: not-allowed;
}
/* Devices Panel */
.devices-panel h3,
.sessions-panel h3 {
margin-bottom: 15px;
color: #2196F3;
font-size: 14px;
/* Devices and Sessions Panels */
.devices-panel,
.sessions-panel {
flex: 1;
min-height: 0;
}
.devices-list,
.sessions-list {
max-height: 200px;
.devices-panel h3,
.sessions-panel h3 {
margin-bottom: 8px;
color: #2196F3;
font-size: 13px;
display: flex;
align-items: center;
gap: 6px;
}
.compact-list {
max-height: 180px;
overflow-y: auto;
transition: max-height 0.3s ease;
}
.control-panel.logs-collapsed .compact-list {
max-height: 250px;
}
.device-item,
.session-item {
background: #3a3a3a;
margin-bottom: 8px;
padding: 10px;
margin-bottom: 6px;
padding: 8px;
border-radius: 4px;
border-left: 3px solid #4CAF50;
transition: all 0.2s ease;
}
.device-item:hover,
.session-item:hover {
background: #404040;
transform: translateY(-1px);
}
.device-info {
font-size: 12px;
margin-bottom: 5px;
font-size: 11px;
margin-bottom: 4px;
line-height: 1.3;
}
.device-info strong {
@@ -418,28 +511,56 @@ button:disabled {
}
.device-capabilities {
font-size: 11px;
font-size: 10px;
color: #888;
margin-bottom: 8px;
margin-bottom: 6px;
line-height: 1.2;
}
.device-actions {
display: flex;
gap: 5px;
gap: 4px;
flex-wrap: wrap;
}
.btn-device {
background: #2196F3;
border: none;
color: white;
padding: 4px 8px;
padding: 3px 6px;
border-radius: 3px;
cursor: pointer;
font-size: 10px;
font-size: 9px;
transition: all 0.2s ease;
}
.btn-device:hover {
background: #1976D2;
transform: scale(1.05);
}
.btn-device.btn-success {
background: #4CAF50;
}
.btn-device.btn-success:hover {
background: #45a049;
}
.btn-icon {
background: none;
border: none;
color: #888;
cursor: pointer;
font-size: 14px;
padding: 2px;
border-radius: 3px;
transition: all 0.2s ease;
}
.btn-icon:hover {
color: #fff;
background: rgba(255, 255, 255, 0.1);
}
.no-devices,
@@ -450,29 +571,119 @@ button:disabled {
padding: 20px;
}
/* Session styles */
.session-item {
border-left: 3px solid #2196F3;
}
.session-item.session-pending {
border-left-color: #FF9800;
}
.session-item.session-active {
border-left-color: #4CAF50;
}
.session-item.session-rejected {
border-left-color: #f44336;
}
.session-item.session-ended {
border-left-color: #666;
opacity: 0.7;
}
.session-item.active {
background: #4a4a4a;
border: 2px solid #4CAF50;
border-left: 3px solid #4CAF50;
}
.session-header {
font-size: 12px;
margin-bottom: 8px;
}
.session-header strong {
color: #4CAF50;
}
.status-pending { color: #FF9800; }
.status-active { color: #4CAF50; }
.status-rejected { color: #f44336; }
.status-ended { color: #666; }
.session-actions {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.btn-small {
padding: 2px 6px;
font-size: 10px;
border: none;
border-radius: 2px;
cursor: pointer;
color: white;
}
.btn-small.btn-primary {
background: #2196F3;
}
.btn-small.btn-success {
background: #4CAF50;
}
.btn-small.btn-secondary {
background: #757575;
}
.btn-small.btn-danger {
background: #f44336;
}
.btn-small:hover {
opacity: 0.8;
}
/* Logs Panel */
.logs-panel {
flex: 1;
display: flex;
flex-direction: column;
transition: all 0.3s ease;
}
.logs-panel.collapsed {
flex: none;
}
.logs-panel h3 {
margin-bottom: 15px;
margin-bottom: 8px;
color: #FF9800;
font-size: 14px;
font-size: 13px;
display: flex;
align-items: center;
gap: 6px;
}
.logs-container {
flex: 1;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 4px;
padding: 8px;
padding: 6px;
font-family: 'Courier New', monospace;
font-size: 11px;
font-size: 10px;
overflow-y: auto;
margin-bottom: 10px;
margin-bottom: 8px;
max-height: 120px;
transition: max-height 0.3s ease;
}
.control-panel.logs-collapsed .logs-container {
max-height: 0;
padding: 0;
margin: 0;
border: none;
}
.log-entry {