67437a0c46
Users logged into Alta in Chrome can now send their session cookie to the running Electron app via a local HTTP server on port 18247, eliminating the need for re-authentication. - main.js: HTTP cookie server with CORS, token, domain validation - preload.js: onExtensionCookie push-pattern IPC bridge - renderer.js: handleExtensionCookie sets session, fetches devices - chrome-extension/: Manifest V3 extension with popup UI - CLAUDE.md: updated architecture docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1102 lines
40 KiB
JavaScript
1102 lines
40 KiB
JavaScript
// Global variables to store session data
|
|
let sessionData = {
|
|
deploymentUrl: '',
|
|
cookies: null,
|
|
isConnected: false
|
|
};
|
|
|
|
// DOM elements
|
|
const connectBtn = document.getElementById('connectBtn');
|
|
const connectionStatus = document.getElementById('connectionStatus');
|
|
const deviceStatus = document.getElementById('deviceStatus');
|
|
const deviceList = document.getElementById('deviceList');
|
|
const deviceDetails = document.getElementById('deviceDetails');
|
|
const statusIndicator = document.getElementById('statusIndicator');
|
|
const deviceUUID = document.getElementById('deviceUUID');
|
|
const deviceSearch = document.getElementById('deviceSearch');
|
|
|
|
// New buttons for the layout
|
|
const testConnectionBtn = document.getElementById('testConnectionBtn');
|
|
const disconnectBtn = document.getElementById('disconnectBtn');
|
|
const startProxyBtn = document.getElementById('startProxyBtn');
|
|
const checkVersionBtn = document.getElementById('checkVersionBtn');
|
|
const stopProxyBtn = document.getElementById('stopProxyBtn');
|
|
|
|
// Cookie proxy elements
|
|
const cookieDeviceUUID = document.getElementById('cookieDeviceUUID');
|
|
const cookieKey = document.getElementById('cookieKey');
|
|
const startCookieProxyBtn = document.getElementById('startCookieProxyBtn');
|
|
const stopCookieProxyBtn = document.getElementById('stopCookieProxyBtn');
|
|
|
|
// Profile management elements
|
|
const profileSelect = document.getElementById('profileSelect');
|
|
const addProfileBtn = document.getElementById('addProfileBtn');
|
|
const manageProfilesBtn = document.getElementById('manageProfilesBtn');
|
|
const addProfileModal = document.getElementById('addProfileModal');
|
|
const manageProfilesModal = document.getElementById('manageProfilesModal');
|
|
const addProfileForm = document.getElementById('addProfileForm');
|
|
const profilesList = document.getElementById('profilesList');
|
|
|
|
// Track selected device and profiles
|
|
let selectedDevice = null;
|
|
let currentProfiles = [];
|
|
let selectedProfile = null;
|
|
let activeProxyConnections = new Map(); // Track multiple active connections
|
|
let activeCookieProxyConnections = new Map(); // Track cookie-based proxy connections
|
|
const MAX_PROXY_CONNECTIONS = 2; // Limit to 2 simultaneous connections
|
|
let allDevices = []; // Store all devices for search functionality
|
|
|
|
// Event listeners
|
|
connectBtn.addEventListener('click', handleConnection);
|
|
testConnectionBtn.addEventListener('click', handleTestConnection);
|
|
disconnectBtn.addEventListener('click', handleDisconnect);
|
|
startProxyBtn.addEventListener('click', handleStartProxy);
|
|
checkVersionBtn.addEventListener('click', handleCheckVersion);
|
|
stopProxyBtn.addEventListener('click', handleStopProxy);
|
|
|
|
// Cookie proxy event listeners
|
|
startCookieProxyBtn.addEventListener('click', handleStartCookieProxy);
|
|
stopCookieProxyBtn.addEventListener('click', handleStopCookieProxy);
|
|
|
|
// Cookie key input listener to update button states
|
|
cookieKey.addEventListener('input', updateCookieProxyButtonStates);
|
|
|
|
// Device search event listener
|
|
deviceSearch.addEventListener('input', handleDeviceSearch);
|
|
|
|
// Cookie section collapsible header
|
|
document.getElementById('cookieSectionHeader').addEventListener('click', toggleCookieSection);
|
|
|
|
// Profile management event listeners
|
|
profileSelect.addEventListener('change', handleProfileSelection);
|
|
addProfileBtn.addEventListener('click', showAddProfileModal);
|
|
manageProfilesBtn.addEventListener('click', showManageProfilesModal);
|
|
addProfileForm.addEventListener('submit', handleAddProfile);
|
|
|
|
// Modal event listeners
|
|
document.getElementById('closeAddProfile').addEventListener('click', hideAddProfileModal);
|
|
document.getElementById('cancelAddProfile').addEventListener('click', hideAddProfileModal);
|
|
document.getElementById('closeManageProfiles').addEventListener('click', hideManageProfilesModal);
|
|
document.getElementById('closeManageProfilesBtn').addEventListener('click', hideManageProfilesModal);
|
|
|
|
// Close modals when clicking outside
|
|
addProfileModal.addEventListener('click', (e) => {
|
|
if (e.target === addProfileModal) hideAddProfileModal();
|
|
});
|
|
manageProfilesModal.addEventListener('click', (e) => {
|
|
if (e.target === manageProfilesModal) hideManageProfilesModal();
|
|
});
|
|
|
|
// Handle connection button click
|
|
async function handleConnection() {
|
|
if (!selectedProfile) {
|
|
showStatus(connectionStatus, 'Please select a profile to connect', 'error');
|
|
return;
|
|
}
|
|
|
|
// Disable button during connection
|
|
setConnectButtonEnabled(false);
|
|
showStatus(connectionStatus, 'Connecting to Alta API...', 'info');
|
|
|
|
try {
|
|
const result = await window.electronAPI.login({
|
|
deploymentUrl: selectedProfile.deploymentUrl,
|
|
username: selectedProfile.username,
|
|
password: selectedProfile.password
|
|
});
|
|
|
|
if (result.success) {
|
|
// Store session data
|
|
sessionData.deploymentUrl = selectedProfile.deploymentUrl;
|
|
sessionData.cookies = result.cookies;
|
|
sessionData.isConnected = true;
|
|
|
|
showStatus(connectionStatus, 'Connected successfully!', 'success');
|
|
updateConnectionStatus(true);
|
|
updateButtonStates();
|
|
|
|
// Auto-retrieve devices when connected
|
|
try {
|
|
await handleGetDevices();
|
|
} catch (deviceError) {
|
|
console.error('Failed to auto-fetch devices:', deviceError);
|
|
showStatus(deviceStatus, 'Connected, but failed to load devices. Try selecting a profile and reconnecting.', 'warning');
|
|
}
|
|
|
|
} else {
|
|
showStatus(connectionStatus, `Connection failed: ${result.message}`, 'error');
|
|
setConnectButtonEnabled(true);
|
|
}
|
|
} catch (error) {
|
|
console.error('Connection error:', error);
|
|
showStatus(connectionStatus, `Connection error: ${error.message}`, 'error');
|
|
setConnectButtonEnabled(true);
|
|
}
|
|
}
|
|
|
|
// Handle test connection
|
|
async function handleTestConnection() {
|
|
if (!sessionData.isConnected) {
|
|
showStatus(connectionStatus, 'Please connect to API first', 'error');
|
|
return;
|
|
}
|
|
|
|
showStatus(connectionStatus, 'Testing connection...', 'info');
|
|
|
|
try {
|
|
const result = await window.electronAPI.getAuthInfo({
|
|
deploymentUrl: sessionData.deploymentUrl,
|
|
cookies: sessionData.cookies
|
|
});
|
|
|
|
if (result.success) {
|
|
showStatus(connectionStatus, 'Connection test successful!', 'success');
|
|
} else {
|
|
showStatus(connectionStatus, `Connection test failed: ${result.message}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Test connection error:', error);
|
|
showStatus(connectionStatus, `Test failed: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// Handle disconnect
|
|
function handleDisconnect() {
|
|
sessionData.isConnected = false;
|
|
sessionData.cookies = null;
|
|
sessionData.deploymentUrl = '';
|
|
selectedDevice = null;
|
|
activeProxyConnections.clear();
|
|
activeCookieProxyConnections.clear();
|
|
allDevices = []; // Clear stored devices
|
|
|
|
updateConnectionStatus(false);
|
|
updateButtonStates();
|
|
clearDeviceList();
|
|
deviceUUID.value = '';
|
|
cookieDeviceUUID.value = '';
|
|
cookieKey.value = '';
|
|
deviceSearch.value = ''; // Clear search input
|
|
|
|
// Clear device status message
|
|
deviceStatus.style.display = 'none';
|
|
deviceStatus.textContent = '';
|
|
|
|
// Reset connect button text if it was stuck
|
|
connectBtn.textContent = 'Connect to API';
|
|
connectBtn.disabled = !selectedProfile;
|
|
|
|
// Reset proxy buttons
|
|
startProxyBtn.disabled = true;
|
|
stopProxyBtn.disabled = true;
|
|
|
|
showStatus(connectionStatus, 'Disconnected from API', 'info');
|
|
}
|
|
|
|
// Handle start proxy
|
|
async function handleStartProxy() {
|
|
if (!selectedDevice) {
|
|
showStatus(connectionStatus, 'Please select a device first', 'error');
|
|
return;
|
|
}
|
|
|
|
if (!selectedProfile) {
|
|
showStatus(connectionStatus, 'Please select a profile first', 'error');
|
|
return;
|
|
}
|
|
|
|
// Check connection limit
|
|
if (activeProxyConnections.size >= MAX_PROXY_CONNECTIONS) {
|
|
showStatus(connectionStatus, `Maximum of ${MAX_PROXY_CONNECTIONS} camera proxy connections allowed. Stop an existing connection first.`, 'warning');
|
|
return;
|
|
}
|
|
|
|
// Check if this device already has an active connection
|
|
const deviceId = selectedDevice.guid || selectedDevice.id;
|
|
if (activeProxyConnections.has(deviceId)) {
|
|
showStatus(connectionStatus, `Camera proxy already running for device ${selectedDevice.name}`, 'warning');
|
|
return;
|
|
}
|
|
|
|
// Check if camera proxy executable exists
|
|
try {
|
|
const checkResult = await window.electronAPI.checkCameraProxy();
|
|
if (!checkResult.exists) {
|
|
showStatus(connectionStatus, 'Camera proxy executable (aware-cam-proxy-win.exe) not found in application directory', 'error');
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
showStatus(connectionStatus, 'Failed to check camera proxy executable', 'error');
|
|
return;
|
|
}
|
|
|
|
// Disable button during launch
|
|
startProxyBtn.disabled = true;
|
|
showStatus(connectionStatus, `Starting camera proxy for device ${selectedDevice.name}...`, 'info');
|
|
|
|
try {
|
|
const result = await window.electronAPI.launchCameraProxy({
|
|
deploymentUrl: selectedProfile.deploymentUrl,
|
|
username: selectedProfile.username,
|
|
password: selectedProfile.password,
|
|
deviceUuid: deviceId
|
|
});
|
|
|
|
if (result.success) {
|
|
// Track this connection
|
|
activeProxyConnections.set(deviceId, {
|
|
processId: result.processId,
|
|
deviceName: selectedDevice.name,
|
|
deviceId: deviceId,
|
|
startTime: Date.now()
|
|
});
|
|
|
|
updateProxyButtonStates();
|
|
showStatus(connectionStatus, `${result.message} (${activeProxyConnections.size}/${MAX_PROXY_CONNECTIONS} connections active)`, 'success');
|
|
} else {
|
|
showStatus(connectionStatus, `Failed to start camera proxy: ${result.message}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Camera proxy launch error:', error);
|
|
showStatus(connectionStatus, `Error launching camera proxy: ${error.message}`, 'error');
|
|
} finally {
|
|
startProxyBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
// Handle check version
|
|
async function handleCheckVersion() {
|
|
showStatus(connectionStatus, 'Checking camera proxy version...', 'info');
|
|
|
|
try {
|
|
const result = await window.electronAPI.getCameraProxyVersion();
|
|
|
|
if (result.success) {
|
|
showStatus(connectionStatus, `Camera Proxy Version: ${result.version}`, 'info');
|
|
} else {
|
|
showStatus(connectionStatus, `Failed to get version: ${result.message}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Version check error:', error);
|
|
showStatus(connectionStatus, `Error checking version: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// Handle stop proxy
|
|
async function handleStopProxy() {
|
|
if (activeProxyConnections.size === 0) {
|
|
showStatus(connectionStatus, 'No active camera proxy connections found', 'warning');
|
|
return;
|
|
}
|
|
|
|
showStatus(connectionStatus, 'Stopping all camera proxy connections...', 'info');
|
|
|
|
try {
|
|
const result = await window.electronAPI.stopCameraProxy(null); // Stop all processes
|
|
|
|
if (result.success) {
|
|
activeProxyConnections.clear();
|
|
showStatus(connectionStatus, 'All camera proxy connections stopped successfully', 'success');
|
|
|
|
// Update visual indicators for all devices
|
|
const deviceItems = deviceList.querySelectorAll('.device-item');
|
|
deviceItems.forEach(item => {
|
|
item.classList.remove('proxy-active');
|
|
});
|
|
} else {
|
|
showStatus(connectionStatus, `Failed to stop camera proxy: ${result.message}`, 'warning');
|
|
}
|
|
} catch (error) {
|
|
console.error('Stop proxy error:', error);
|
|
showStatus(connectionStatus, 'Error stopping camera proxy', 'error');
|
|
}
|
|
|
|
updateProxyButtonStates();
|
|
updateCookieProxyButtonStates();
|
|
}
|
|
|
|
// Handle start cookie proxy
|
|
async function handleStartCookieProxy() {
|
|
if (!selectedDevice) {
|
|
showStatus(connectionStatus, 'Please select a device first', 'error');
|
|
return;
|
|
}
|
|
|
|
const cookieKeyValue = cookieKey.value.trim();
|
|
if (!cookieKeyValue) {
|
|
showStatus(connectionStatus, 'Please enter a cookie key', 'error');
|
|
return;
|
|
}
|
|
|
|
if (!sessionData.deploymentUrl) {
|
|
showStatus(connectionStatus, 'Please connect to API first to get deployment URL', 'error');
|
|
return;
|
|
}
|
|
|
|
// Check if this device already has an active cookie connection
|
|
const deviceId = selectedDevice.guid || selectedDevice.id;
|
|
if (activeCookieProxyConnections.has(deviceId)) {
|
|
showStatus(connectionStatus, `Cookie proxy already running for device ${selectedDevice.name}`, 'warning');
|
|
return;
|
|
}
|
|
|
|
// Disable button during launch
|
|
startCookieProxyBtn.disabled = true;
|
|
showStatus(connectionStatus, `Starting cookie proxy for device ${selectedDevice.name}...`, 'info');
|
|
|
|
try {
|
|
const result = await window.electronAPI.launchCookieCameraProxy({
|
|
deploymentUrl: sessionData.deploymentUrl,
|
|
cookieKey: cookieKeyValue,
|
|
deviceUuid: deviceId
|
|
});
|
|
|
|
if (result.success) {
|
|
// Track this connection
|
|
activeCookieProxyConnections.set(deviceId, {
|
|
processId: result.processId,
|
|
deviceName: selectedDevice.name,
|
|
deviceId: deviceId,
|
|
startTime: Date.now(),
|
|
type: 'cookie'
|
|
});
|
|
|
|
updateCookieProxyButtonStates();
|
|
showStatus(connectionStatus, `${result.message} (Cookie proxy active for ${selectedDevice.name})`, 'success');
|
|
} else {
|
|
showStatus(connectionStatus, `Failed to start cookie proxy: ${result.message}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Cookie proxy launch error:', error);
|
|
showStatus(connectionStatus, `Error launching cookie proxy: ${error.message}`, 'error');
|
|
} finally {
|
|
startCookieProxyBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
// Handle stop cookie proxy
|
|
async function handleStopCookieProxy() {
|
|
if (activeCookieProxyConnections.size === 0) {
|
|
showStatus(connectionStatus, 'No active cookie proxy connections found', 'warning');
|
|
return;
|
|
}
|
|
|
|
showStatus(connectionStatus, 'Stopping cookie proxy connections...', 'info');
|
|
|
|
try {
|
|
const result = await window.electronAPI.stopCameraProxy(null); // Stop all processes
|
|
|
|
if (result.success) {
|
|
activeCookieProxyConnections.clear();
|
|
showStatus(connectionStatus, 'Cookie proxy connections stopped successfully', 'success');
|
|
|
|
// Update visual indicators for all devices
|
|
const deviceItems = deviceList.querySelectorAll('.device-item');
|
|
deviceItems.forEach(item => {
|
|
item.classList.remove('cookie-proxy-active');
|
|
});
|
|
} else {
|
|
showStatus(connectionStatus, `Failed to stop cookie proxy: ${result.message}`, 'warning');
|
|
}
|
|
} catch (error) {
|
|
console.error('Stop cookie proxy error:', error);
|
|
showStatus(connectionStatus, 'Error stopping cookie proxy', 'error');
|
|
}
|
|
|
|
updateCookieProxyButtonStates();
|
|
}
|
|
|
|
// Update cookie proxy button states
|
|
function updateCookieProxyButtonStates() {
|
|
const hasSelectedDevice = selectedDevice !== null;
|
|
const hasCookieKey = cookieKey.value.trim().length > 0;
|
|
const hasActiveConnection = activeCookieProxyConnections.size > 0;
|
|
|
|
if (sessionData.isConnected && hasSelectedDevice && hasCookieKey) {
|
|
const deviceId = selectedDevice.guid || selectedDevice.id;
|
|
const isThisDeviceActive = activeCookieProxyConnections.has(deviceId);
|
|
|
|
startCookieProxyBtn.disabled = isThisDeviceActive;
|
|
stopCookieProxyBtn.disabled = !hasActiveConnection;
|
|
} else {
|
|
startCookieProxyBtn.disabled = true;
|
|
stopCookieProxyBtn.disabled = !hasActiveConnection;
|
|
}
|
|
}
|
|
|
|
// Update connection status indicator
|
|
function updateConnectionStatus(connected) {
|
|
const statusDot = statusIndicator.querySelector('.status-dot');
|
|
const statusText = statusIndicator.querySelector('.status-text');
|
|
|
|
if (connected) {
|
|
statusDot.className = 'status-dot online';
|
|
statusText.textContent = 'Connected';
|
|
} else {
|
|
statusDot.className = 'status-dot offline';
|
|
statusText.textContent = 'Disconnected';
|
|
}
|
|
}
|
|
|
|
// Update button states based on connection status
|
|
function updateButtonStates() {
|
|
if (sessionData.isConnected) {
|
|
connectBtn.style.display = 'none'; // Hide connect button when connected
|
|
testConnectionBtn.disabled = false;
|
|
disconnectBtn.disabled = false;
|
|
checkVersionBtn.disabled = false;
|
|
updateProxyButtonStates(); // Update proxy buttons when connected
|
|
} else {
|
|
connectBtn.style.display = 'inline-block'; // Show connect button when disconnected
|
|
connectBtn.disabled = !selectedProfile;
|
|
testConnectionBtn.disabled = true;
|
|
disconnectBtn.disabled = true;
|
|
startProxyBtn.disabled = true;
|
|
checkVersionBtn.disabled = true;
|
|
stopProxyBtn.disabled = true;
|
|
startCookieProxyBtn.disabled = true;
|
|
stopCookieProxyBtn.disabled = true;
|
|
}
|
|
|
|
// Always update cookie proxy button states
|
|
updateCookieProxyButtonStates();
|
|
}
|
|
|
|
// Handle get devices (now called automatically)
|
|
async function handleGetDevices() {
|
|
if (!sessionData.isConnected) {
|
|
showStatus(deviceStatus, 'Please connect to the API first', 'error');
|
|
return;
|
|
}
|
|
|
|
showStatus(deviceStatus, 'Fetching devices...', 'info');
|
|
clearDeviceList();
|
|
|
|
try {
|
|
const result = await window.electronAPI.getDevices({
|
|
deploymentUrl: sessionData.deploymentUrl,
|
|
cookies: sessionData.cookies
|
|
});
|
|
|
|
if (result.success) {
|
|
// Filter devices to only show non-cloud cameras (localStorage = false)
|
|
const filteredDevices = result.devices.filter(device => {
|
|
// Check if device has capabilities and localStorage property
|
|
if (device.capabilities && device.capabilities.localStorage !== undefined) {
|
|
// Only show devices where localStorage is false (non-cloud cameras)
|
|
return device.capabilities.localStorage === false;
|
|
}
|
|
// If no capabilities or localStorage property, include the device (fallback)
|
|
return true;
|
|
});
|
|
|
|
const totalDevices = result.devices.length;
|
|
const filteredCount = filteredDevices.length;
|
|
const cloudDevicesHidden = totalDevices - filteredCount;
|
|
|
|
let statusMessage = `Found ${filteredCount} local camera${filteredCount !== 1 ? 's' : ''}`;
|
|
if (cloudDevicesHidden > 0) {
|
|
statusMessage += ` (${cloudDevicesHidden} cloud camera${cloudDevicesHidden !== 1 ? 's' : ''} hidden)`;
|
|
}
|
|
|
|
showStatus(deviceStatus, statusMessage, 'success');
|
|
|
|
// Store all devices for search functionality
|
|
allDevices = filteredDevices;
|
|
displayDevices(filteredDevices);
|
|
} else {
|
|
showStatus(deviceStatus, `Failed to get devices: ${result.message}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Get devices error:', error);
|
|
showStatus(deviceStatus, `Error getting devices: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// Helper function to determine device status from API data
|
|
function getDeviceStatus(device) {
|
|
// Check for live.display_status first (Alta API standard)
|
|
if (device.live && device.live.display_status) {
|
|
const status = device.live.display_status.toLowerCase();
|
|
|
|
// Handle color-based status responses from Alta API
|
|
if (status === 'green') {
|
|
return {
|
|
isOnline: true,
|
|
statusText: 'Online'
|
|
};
|
|
} else if (status === 'red') {
|
|
return {
|
|
isOnline: false,
|
|
statusText: 'Offline'
|
|
};
|
|
} else if (status === 'yellow' || status === 'orange') {
|
|
return {
|
|
isOnline: false,
|
|
statusText: 'Warning'
|
|
};
|
|
}
|
|
|
|
// Handle text-based status responses
|
|
return {
|
|
isOnline: status === 'online' || status === 'live' || status === 'connected',
|
|
statusText: status === 'online' || status === 'live' || status === 'connected' ? 'Online' : 'Offline'
|
|
};
|
|
}
|
|
|
|
// Fallback to other possible status fields
|
|
if (device.online !== undefined) {
|
|
return {
|
|
isOnline: device.online,
|
|
statusText: device.online ? 'Online' : 'Offline'
|
|
};
|
|
}
|
|
|
|
if (device.status) {
|
|
const status = device.status.toLowerCase();
|
|
|
|
// Handle color-based status in other fields
|
|
if (status === 'green') {
|
|
return {
|
|
isOnline: true,
|
|
statusText: 'Online'
|
|
};
|
|
} else if (status === 'red') {
|
|
return {
|
|
isOnline: false,
|
|
statusText: 'Offline'
|
|
};
|
|
}
|
|
|
|
return {
|
|
isOnline: status === 'online' || status === 'live' || status === 'connected',
|
|
statusText: status === 'online' || status === 'live' || status === 'connected' ? 'Online' : 'Offline'
|
|
};
|
|
}
|
|
|
|
// Default to offline if no status information available
|
|
return {
|
|
isOnline: false,
|
|
statusText: 'Offline'
|
|
};
|
|
}
|
|
|
|
// Handle device search
|
|
function handleDeviceSearch() {
|
|
const searchTerm = deviceSearch.value.toLowerCase().trim();
|
|
|
|
if (!searchTerm) {
|
|
// Show all devices if search is empty
|
|
displayDevices(allDevices);
|
|
return;
|
|
}
|
|
|
|
// Filter devices based on search term
|
|
const filteredDevices = allDevices.filter(device => {
|
|
const deviceName = (device.name || '').toLowerCase();
|
|
const deviceId = (device.guid || device.id || '').toLowerCase();
|
|
const deviceType = (device.type || '').toLowerCase();
|
|
const deviceModel = (device.model || '').toLowerCase();
|
|
const deviceIp = (device.ipAddress || '').toLowerCase();
|
|
|
|
return deviceName.includes(searchTerm) ||
|
|
deviceId.includes(searchTerm) ||
|
|
deviceType.includes(searchTerm) ||
|
|
deviceModel.includes(searchTerm) ||
|
|
deviceIp.includes(searchTerm);
|
|
});
|
|
|
|
displayDevices(filteredDevices);
|
|
}
|
|
|
|
// Display devices in the UI
|
|
function displayDevices(devices) {
|
|
clearDeviceList();
|
|
|
|
if (!devices || devices.length === 0) {
|
|
const searchTerm = deviceSearch.value.toLowerCase().trim();
|
|
const message = searchTerm ? 'No devices match your search' : 'No devices found';
|
|
deviceList.innerHTML = `<p class="no-devices">${message}</p>`;
|
|
return;
|
|
}
|
|
|
|
devices.forEach((device, index) => {
|
|
const deviceItem = document.createElement('div');
|
|
deviceItem.className = 'device-item';
|
|
deviceItem.dataset.deviceIndex = index;
|
|
deviceItem.dataset.deviceId = device.guid || device.id;
|
|
|
|
const deviceStatus = getDeviceStatus(device);
|
|
const deviceId = device.guid || device.id;
|
|
const isProxyActive = activeProxyConnections.has(deviceId);
|
|
|
|
// Add proxy-active class if this device has an active connection
|
|
if (isProxyActive) {
|
|
deviceItem.classList.add('proxy-active');
|
|
}
|
|
|
|
deviceItem.innerHTML = `
|
|
<div class="device-name">${escapeHtml(device.name || 'Unnamed Device')}</div>
|
|
<div class="device-status-dot ${deviceStatus.isOnline ? 'online' : 'offline'}"></div>
|
|
`;
|
|
|
|
// Add click handler for device selection
|
|
deviceItem.addEventListener('click', () => selectDevice(device, deviceItem));
|
|
|
|
deviceList.appendChild(deviceItem);
|
|
});
|
|
|
|
// Update proxy button states after displaying devices
|
|
updateProxyButtonStates();
|
|
}
|
|
|
|
// Handle device selection
|
|
function selectDevice(device, deviceElement) {
|
|
// Remove previous selection
|
|
const previousSelected = deviceList.querySelector('.device-item.selected');
|
|
if (previousSelected) {
|
|
previousSelected.classList.remove('selected');
|
|
}
|
|
|
|
// Select current device
|
|
deviceElement.classList.add('selected');
|
|
selectedDevice = device;
|
|
|
|
// Auto-populate device UUID fields
|
|
const uuid = device.guid || device.id || '';
|
|
deviceUUID.value = uuid;
|
|
cookieDeviceUUID.value = uuid; // Also populate cookie proxy UUID field
|
|
|
|
// Show device selection feedback with connection status
|
|
if (uuid) {
|
|
const isActive = activeProxyConnections.has(uuid);
|
|
const isCookieActive = activeCookieProxyConnections.has(uuid);
|
|
const statusText = isActive ? ' (PROXY ACTIVE)' : '';
|
|
const cookieStatusText = isCookieActive ? ' (COOKIE PROXY ACTIVE)' : '';
|
|
showStatus(connectionStatus, `Selected device: ${device.name || 'Unnamed Device'} (UUID: ${uuid})${statusText}${cookieStatusText}`, 'info');
|
|
}
|
|
|
|
updateProxyButtonStates();
|
|
updateCookieProxyButtonStates();
|
|
}
|
|
|
|
// Display detailed device information
|
|
function displayDeviceDetails(device) {
|
|
const deviceStatus = getDeviceStatus(device);
|
|
|
|
const detailsHtml = `
|
|
<div class="device-details-card">
|
|
<div class="device-details-header">
|
|
<h3 class="device-details-name">${escapeHtml(device.name || 'Unnamed Device')}</h3>
|
|
<span class="device-status ${deviceStatus.isOnline ? 'online' : 'offline'}">
|
|
${escapeHtml(deviceStatus.statusText)}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="device-details-grid">
|
|
<div class="device-detail-item">
|
|
<div class="device-detail-label">Device ID</div>
|
|
<div class="device-detail-value">${escapeHtml(device.guid || device.id || 'N/A')}</div>
|
|
</div>
|
|
|
|
<div class="device-detail-item">
|
|
<div class="device-detail-label">Device Type</div>
|
|
<div class="device-detail-value">${escapeHtml(device.type || 'Unknown')}</div>
|
|
</div>
|
|
|
|
<div class="device-detail-item">
|
|
<div class="device-detail-label">Model</div>
|
|
<div class="device-detail-value">${escapeHtml(device.model || 'Unknown')}</div>
|
|
</div>
|
|
|
|
<div class="device-detail-item">
|
|
<div class="device-detail-label">IP Address</div>
|
|
<div class="device-detail-value">${escapeHtml(device.ipAddress || 'N/A')}</div>
|
|
</div>
|
|
|
|
<div class="device-detail-item">
|
|
<div class="device-detail-label">MAC Address</div>
|
|
<div class="device-detail-value">${escapeHtml(device.macAddress || 'N/A')}</div>
|
|
</div>
|
|
|
|
<div class="device-detail-item">
|
|
<div class="device-detail-label">Firmware Version</div>
|
|
<div class="device-detail-value">${escapeHtml(device.firmwareVersion || 'N/A')}</div>
|
|
</div>
|
|
|
|
<div class="device-detail-item">
|
|
<div class="device-detail-label">Serial Number</div>
|
|
<div class="device-detail-value">${escapeHtml(device.serialNumber || 'N/A')}</div>
|
|
</div>
|
|
|
|
<div class="device-detail-item">
|
|
<div class="device-detail-label">Location</div>
|
|
<div class="device-detail-value">${escapeHtml(device.location || 'N/A')}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
deviceDetails.innerHTML = detailsHtml;
|
|
}
|
|
|
|
// Utility functions
|
|
function showStatus(element, message, type) {
|
|
element.textContent = message;
|
|
element.className = `status-message ${type}`;
|
|
element.style.display = 'block';
|
|
}
|
|
|
|
function setConnectButtonEnabled(enabled) {
|
|
connectBtn.disabled = !enabled;
|
|
connectBtn.textContent = enabled ? 'Connect to Alta API' : 'Connecting...';
|
|
}
|
|
|
|
function clearDeviceList() {
|
|
deviceList.innerHTML = '<p class="placeholder-text">Connect to API to load devices</p>';
|
|
selectedDevice = null;
|
|
deviceUUID.value = '';
|
|
}
|
|
|
|
// New function to update proxy button states based on active connections
|
|
function updateProxyButtonStates() {
|
|
if (!sessionData.isConnected) {
|
|
startProxyBtn.disabled = true;
|
|
stopProxyBtn.disabled = true;
|
|
return;
|
|
}
|
|
|
|
const hasActiveConnections = activeProxyConnections.size > 0;
|
|
const atMaxConnections = activeProxyConnections.size >= MAX_PROXY_CONNECTIONS;
|
|
const selectedDeviceActive = selectedDevice && activeProxyConnections.has(selectedDevice.guid || selectedDevice.id);
|
|
|
|
// Enable start button if: connected, device selected, not at max connections, and device not already active
|
|
startProxyBtn.disabled = !selectedDevice || atMaxConnections || selectedDeviceActive;
|
|
|
|
// Enable stop button if there are active connections
|
|
stopProxyBtn.disabled = !hasActiveConnections;
|
|
|
|
// Update button text to show connection count
|
|
if (hasActiveConnections) {
|
|
stopProxyBtn.textContent = `Stop All Proxies (${activeProxyConnections.size})`;
|
|
} else {
|
|
stopProxyBtn.textContent = 'Stop Proxy';
|
|
}
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
if (typeof text !== 'string') return text;
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Profile Management Functions
|
|
async function loadProfiles() {
|
|
try {
|
|
const result = await window.electronAPI.loadProfiles();
|
|
if (result.success) {
|
|
currentProfiles = result.profiles;
|
|
updateProfileDropdown();
|
|
} else {
|
|
console.error('Failed to load profiles:', result.message);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading profiles:', error);
|
|
}
|
|
}
|
|
|
|
function updateProfileDropdown() {
|
|
// Clear existing options except the first one
|
|
profileSelect.innerHTML = '<option value="">Select a profile...</option>';
|
|
|
|
currentProfiles.forEach(profile => {
|
|
const option = document.createElement('option');
|
|
option.value = profile.id;
|
|
option.textContent = `${profile.name} (${profile.username})`;
|
|
profileSelect.appendChild(option);
|
|
});
|
|
}
|
|
|
|
async function handleProfileSelection() {
|
|
const selectedProfileId = profileSelect.value;
|
|
if (!selectedProfileId) {
|
|
selectedProfile = null;
|
|
updateButtonStates();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = await window.electronAPI.getProfile(selectedProfileId);
|
|
if (result.success) {
|
|
selectedProfile = result.profile;
|
|
updateButtonStates();
|
|
} else {
|
|
showStatus(connectionStatus, `Failed to load profile: ${result.message}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading profile:', error);
|
|
showStatus(connectionStatus, 'Error loading profile', 'error');
|
|
}
|
|
}
|
|
|
|
function showAddProfileModal() {
|
|
// Clear the form for new profile
|
|
addProfileForm.reset();
|
|
|
|
addProfileModal.style.display = 'block';
|
|
document.getElementById('profileName').focus();
|
|
}
|
|
|
|
function hideAddProfileModal() {
|
|
addProfileModal.style.display = 'none';
|
|
addProfileForm.reset();
|
|
}
|
|
|
|
async function handleAddProfile(event) {
|
|
event.preventDefault();
|
|
|
|
const name = document.getElementById('profileName').value.trim();
|
|
const deploymentUrl = document.getElementById('profileUrl').value.trim();
|
|
const username = document.getElementById('profileUsername').value.trim();
|
|
const password = document.getElementById('profilePassword').value;
|
|
|
|
if (!name || !deploymentUrl || !username || !password) {
|
|
alert('Please fill in all fields');
|
|
return;
|
|
}
|
|
|
|
// Check if profile name already exists
|
|
if (currentProfiles.some(p => p.name.toLowerCase() === name.toLowerCase())) {
|
|
alert('A profile with this name already exists');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = await window.electronAPI.saveProfile({
|
|
name: name,
|
|
deploymentUrl: deploymentUrl.replace(/\/$/, ''), // Remove trailing slash
|
|
username: username,
|
|
password: password
|
|
});
|
|
|
|
if (result.success) {
|
|
hideAddProfileModal();
|
|
await loadProfiles(); // Reload profiles
|
|
|
|
// Select the newly created profile
|
|
profileSelect.value = result.profile.id;
|
|
await handleProfileSelection();
|
|
|
|
showStatus(connectionStatus, 'Profile saved successfully!', 'success');
|
|
} else {
|
|
alert(`Failed to save profile: ${result.message}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving profile:', error);
|
|
alert('Error saving profile');
|
|
}
|
|
}
|
|
|
|
function showManageProfilesModal() {
|
|
updateProfilesList();
|
|
manageProfilesModal.style.display = 'block';
|
|
}
|
|
|
|
function hideManageProfilesModal() {
|
|
manageProfilesModal.style.display = 'none';
|
|
}
|
|
|
|
function updateProfilesList() {
|
|
profilesList.innerHTML = '';
|
|
|
|
if (currentProfiles.length === 0) {
|
|
profilesList.innerHTML = '<div class="no-profiles">No profiles saved</div>';
|
|
return;
|
|
}
|
|
|
|
currentProfiles.forEach(profile => {
|
|
const profileItem = document.createElement('div');
|
|
profileItem.className = 'profile-item';
|
|
|
|
profileItem.innerHTML = `
|
|
<div class="profile-info">
|
|
<h4>${escapeHtml(profile.name)}</h4>
|
|
<p>${escapeHtml(profile.username)} @ ${escapeHtml(profile.deploymentUrl)}</p>
|
|
</div>
|
|
<div class="profile-actions">
|
|
<button class="profile-action-btn edit">Edit</button>
|
|
<button class="profile-action-btn delete">Delete</button>
|
|
</div>
|
|
`;
|
|
|
|
profileItem.querySelector('.edit').addEventListener('click', () => editProfile(profile.id));
|
|
profileItem.querySelector('.delete').addEventListener('click', () => deleteProfile(profile.id));
|
|
|
|
profilesList.appendChild(profileItem);
|
|
});
|
|
}
|
|
|
|
async function editProfile(profileId) {
|
|
try {
|
|
const result = await window.electronAPI.getProfile(profileId);
|
|
if (result.success) {
|
|
const profile = result.profile;
|
|
const newName = prompt('Enter new profile name:', profile.name);
|
|
if (newName && newName.trim() !== profile.name) {
|
|
const updateResult = await window.electronAPI.updateProfile(profileId, {
|
|
name: newName.trim(),
|
|
deploymentUrl: profile.deploymentUrl,
|
|
username: profile.username,
|
|
password: profile.password
|
|
});
|
|
|
|
if (updateResult.success) {
|
|
await loadProfiles();
|
|
updateProfilesList();
|
|
showStatus(connectionStatus, 'Profile updated successfully!', 'success');
|
|
} else {
|
|
alert(`Failed to update profile: ${updateResult.message}`);
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error editing profile:', error);
|
|
alert('Error editing profile');
|
|
}
|
|
}
|
|
|
|
async function deleteProfile(profileId) {
|
|
const profile = currentProfiles.find(p => p.id === profileId);
|
|
if (!profile) return;
|
|
|
|
if (confirm(`Are you sure you want to delete the profile "${profile.name}"?`)) {
|
|
try {
|
|
const result = await window.electronAPI.deleteProfile(profileId);
|
|
if (result.success) {
|
|
await loadProfiles();
|
|
updateProfilesList();
|
|
|
|
// Clear selection if deleted profile was selected
|
|
if (profileSelect.value === profileId) {
|
|
profileSelect.value = '';
|
|
await handleProfileSelection();
|
|
}
|
|
|
|
showStatus(connectionStatus, 'Profile deleted successfully!', 'success');
|
|
} else {
|
|
alert(`Failed to delete profile: ${result.message}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting profile:', error);
|
|
alert('Error deleting profile');
|
|
}
|
|
}
|
|
}
|
|
|
|
// toggleCookieSection is attached via addEventListener in the event listeners section above
|
|
|
|
// Handle cookie received from Chrome extension via local HTTP bridge
|
|
async function handleExtensionCookie(data) {
|
|
const { deploymentUrl, cookies, cookieValue } = data;
|
|
|
|
// If already connected, disconnect first
|
|
if (sessionData.isConnected) {
|
|
handleDisconnect();
|
|
}
|
|
|
|
// Set session state from extension cookie
|
|
sessionData.deploymentUrl = deploymentUrl;
|
|
sessionData.cookies = cookies;
|
|
sessionData.isConnected = true;
|
|
|
|
showStatus(connectionStatus, `Connected via Chrome extension to ${deploymentUrl}`, 'success');
|
|
updateConnectionStatus(true);
|
|
updateButtonStates();
|
|
|
|
// Auto-populate cookie key and expand cookie proxy section
|
|
cookieKey.value = cookieValue;
|
|
const cookieContent = document.getElementById('cookieProxyContent');
|
|
const cookieIcon = document.getElementById('cookieCollapseIcon');
|
|
if (cookieContent.style.display === 'none') {
|
|
cookieContent.style.display = 'block';
|
|
cookieIcon.textContent = '\u25B2';
|
|
cookieIcon.classList.add('expanded');
|
|
}
|
|
updateCookieProxyButtonStates();
|
|
|
|
// Fetch devices
|
|
try {
|
|
await handleGetDevices();
|
|
} catch (err) {
|
|
console.error('Failed to fetch devices after extension cookie:', err);
|
|
showStatus(deviceStatus, 'Connected, but failed to load devices.', 'warning');
|
|
}
|
|
}
|
|
|
|
// Initialize the app
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
console.log('Alta Video Camera Proxy loaded');
|
|
|
|
// Initialize connection status
|
|
updateConnectionStatus(false);
|
|
updateButtonStates();
|
|
|
|
// Listen for cookies pushed from Chrome extension
|
|
window.electronAPI.onExtensionCookie(handleExtensionCookie);
|
|
|
|
// Load saved profiles
|
|
await loadProfiles();
|
|
|
|
// Check camera proxy executable availability
|
|
await checkCameraProxyAvailability();
|
|
});
|
|
|
|
// Check if camera proxy executable is available
|
|
async function checkCameraProxyAvailability() {
|
|
try {
|
|
const result = await window.electronAPI.checkCameraProxy();
|
|
|
|
if (!result.exists) {
|
|
showStatus(connectionStatus, 'Warning: aware-cam-proxy-win.exe not found. Camera proxy functionality will not work.', 'warning');
|
|
// Disable proxy-related buttons
|
|
startProxyBtn.disabled = true;
|
|
checkVersionBtn.disabled = true;
|
|
|
|
// Add a tooltip or visual indicator
|
|
startProxyBtn.title = 'Camera proxy executable not found';
|
|
checkVersionBtn.title = 'Camera proxy executable not found';
|
|
} else {
|
|
console.log('Camera proxy executable found at:', result.path);
|
|
|
|
// Optionally get version info
|
|
try {
|
|
const versionResult = await window.electronAPI.getCameraProxyVersion();
|
|
if (versionResult.success) {
|
|
console.log('Camera proxy version:', versionResult.version);
|
|
}
|
|
} catch (error) {
|
|
console.log('Could not get camera proxy version:', error);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error checking camera proxy availability:', error);
|
|
showStatus(connectionStatus, 'Error checking camera proxy availability', 'warning');
|
|
}
|
|
}
|
|
|
|
// Toggle cookie section visibility
|
|
function toggleCookieSection() {
|
|
const content = document.getElementById('cookieProxyContent');
|
|
const icon = document.getElementById('cookieCollapseIcon');
|
|
|
|
if (content.style.display === 'none') {
|
|
content.style.display = 'block';
|
|
icon.textContent = '▲';
|
|
icon.classList.add('expanded');
|
|
} else {
|
|
content.style.display = 'none';
|
|
icon.textContent = '▼';
|
|
icon.classList.remove('expanded');
|
|
}
|
|
} |