// Global variables to store session data let sessionData = { deploymentUrl: '', cookies: null, isConnected: false }; // DOM elements const connectionStatus = document.getElementById('connectionStatus'); const deviceStatus = document.getElementById('deviceStatus'); const deviceList = document.getElementById('deviceList'); const statusIndicator = document.getElementById('statusIndicator'); const deviceSearch = document.getElementById('deviceSearch'); // Connection buttons const disconnectBtn = document.getElementById('disconnectBtn'); // Cookie proxy elements const cookieDeviceUUID = document.getElementById('cookieDeviceUUID'); const cookieKey = document.getElementById('cookieKey'); const startCookieProxyBtn = document.getElementById('startCookieProxyBtn'); const stopCookieProxyBtn = document.getElementById('stopCookieProxyBtn'); // Update elements const checkUpdateBtn = document.getElementById('checkUpdateBtn'); const updateModalOverlay = document.getElementById('updateModalOverlay'); const updateModalCloseBtn = document.getElementById('updateModalCloseBtn'); const updateModalMessage = document.getElementById('updateModalMessage'); const updateModalNotes = document.getElementById('updateModalNotes'); const updateProgressContainer = document.getElementById('updateProgressContainer'); const updateProgressFill = document.getElementById('updateProgressFill'); const updateProgressText = document.getElementById('updateProgressText'); const updateInstallBtn = document.getElementById('updateInstallBtn'); const updateLaterBtn = document.getElementById('updateLaterBtn'); // Track selected device let selectedDevice = null; let activeCookieProxyConnections = new Map(); // Track cookie-based proxy connections let allDevices = []; // Store all devices for search functionality let allSites = {}; // Store site id → site name mapping let collapsedSites = new Set(); // Track which site groups are collapsed let pendingUpdateInfo = null; // Store update info for install action // Event listeners disconnectBtn.addEventListener('click', handleDisconnect); // 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); // Update event listeners checkUpdateBtn.addEventListener('click', handleCheckForUpdates); updateInstallBtn.addEventListener('click', handleInstallUpdate); updateLaterBtn.addEventListener('click', closeUpdateModal); updateModalCloseBtn.addEventListener('click', closeUpdateModal); // Handle disconnect function handleDisconnect() { sessionData.isConnected = false; sessionData.cookies = null; sessionData.deploymentUrl = ''; selectedDevice = null; activeCookieProxyConnections.clear(); allDevices = []; // Clear stored devices allSites = {}; // Clear site data collapsedSites.clear(); updateConnectionStatus(false); updateButtonStates(); clearDeviceList(); cookieDeviceUUID.value = ''; cookieKey.value = ''; deviceSearch.value = ''; // Clear search input // Clear device status message deviceStatus.style.display = 'none'; deviceStatus.textContent = ''; showStatus(connectionStatus, 'Disconnected from API', 'info'); } // 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) { disconnectBtn.disabled = false; } else { disconnectBtn.disabled = true; startCookieProxyBtn.disabled = true; stopCookieProxyBtn.disabled = true; } // Always update cookie proxy button states updateCookieProxyButtonStates(); } // Fetch device sites and build id → name map async function fetchDeviceSites() { try { const result = await window.electronAPI.getDeviceSites({ deploymentUrl: sessionData.deploymentUrl, cookies: sessionData.cookies }); if (result.success && Array.isArray(result.sites)) { allSites = {}; result.sites.forEach(site => { if (site.id) { allSites[site.id] = site.name || 'Unnamed Site'; } }); } } catch (error) { console.log('Could not fetch device sites:', error.message); allSites = {}; } } // 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 { // Fetch devices and sites in parallel const [devicesResult] = await Promise.all([ window.electronAPI.getDevices({ deploymentUrl: sessionData.deploymentUrl, cookies: sessionData.cookies }), fetchDeviceSites() ]); if (devicesResult.success) { // Filter devices to only show non-cloud cameras (localStorage = false) const filteredDevices = devicesResult.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 = devicesResult.devices.length; const filteredCount = filteredDevices.length; const cloudDevicesHidden = totalDevices - filteredCount; const siteCount = Object.keys(allSites).length; let statusMessage = `Found ${filteredCount} local camera${filteredCount !== 1 ? 's' : ''}`; if (siteCount > 0) { statusMessage += ` across ${siteCount} site${siteCount !== 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: ${devicesResult.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 (includes site name) 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(); const siteName = (device.server_group_id && allSites[device.server_group_id] || '').toLowerCase(); return deviceName.includes(searchTerm) || deviceId.includes(searchTerm) || deviceType.includes(searchTerm) || deviceModel.includes(searchTerm) || deviceIp.includes(searchTerm) || siteName.includes(searchTerm); }); displayDevices(filteredDevices); } // Group devices by their site using server_group_id → allSites mapping function groupDevicesBySite(devices) { const groups = {}; const ungrouped = []; devices.forEach(device => { const siteId = device.server_group_id; const siteName = siteId && allSites[siteId] ? allSites[siteId] : null; if (siteName) { if (!groups[siteId]) { groups[siteId] = { name: siteName, devices: [] }; } groups[siteId].devices.push(device); } else { ungrouped.push(device); } }); return { groups, ungrouped }; } // Create a device item DOM element function createDeviceItem(device, index) { const deviceItem = document.createElement('div'); deviceItem.className = 'device-item'; deviceItem.dataset.deviceIndex = index; deviceItem.dataset.deviceId = device.guid || device.id; const status = getDeviceStatus(device); const deviceId = device.guid || device.id; const isCookieProxyActive = activeCookieProxyConnections.has(deviceId); if (isCookieProxyActive) { deviceItem.classList.add('cookie-proxy-active'); } deviceItem.innerHTML = `
${message}
`; return; } const hasSites = Object.keys(allSites).length > 0; // If no sites were fetched, fall back to flat list if (!hasSites) { devices.forEach((device, index) => { deviceList.appendChild(createDeviceItem(device, index)); }); updateCookieProxyButtonStates(); return; } const { groups, ungrouped } = groupDevicesBySite(devices); // Sort site groups alphabetically by name const sortedSiteIds = Object.keys(groups).sort((a, b) => groups[a].name.localeCompare(groups[b].name) ); let globalIndex = 0; sortedSiteIds.forEach(siteId => { const group = groups[siteId]; const isCollapsed = collapsedSites.has(siteId); // Site header const siteHeader = document.createElement('div'); siteHeader.className = 'site-group-header' + (isCollapsed ? ' collapsed' : ''); siteHeader.innerHTML = ` ${isCollapsed ? '\u25B6' : '\u25BC'} ${escapeHtml(group.name)} ${group.devices.length} `; siteHeader.addEventListener('click', () => { if (collapsedSites.has(siteId)) { collapsedSites.delete(siteId); } else { collapsedSites.add(siteId); } // Re-render with current search filter const searchTerm = deviceSearch.value.toLowerCase().trim(); if (searchTerm) { handleDeviceSearch(); } else { displayDevices(allDevices); } }); deviceList.appendChild(siteHeader); // Device items (hidden if collapsed) if (!isCollapsed) { group.devices.forEach(device => { deviceList.appendChild(createDeviceItem(device, globalIndex++)); }); } }); // Ungrouped devices at the bottom if (ungrouped.length > 0) { if (sortedSiteIds.length > 0) { const ungroupedHeader = document.createElement('div'); ungroupedHeader.className = 'site-group-header' + (collapsedSites.has('__ungrouped') ? ' collapsed' : ''); ungroupedHeader.innerHTML = ` ${collapsedSites.has('__ungrouped') ? '\u25B6' : '\u25BC'} Ungrouped ${ungrouped.length} `; ungroupedHeader.addEventListener('click', () => { if (collapsedSites.has('__ungrouped')) { collapsedSites.delete('__ungrouped'); } else { collapsedSites.add('__ungrouped'); } const searchTerm = deviceSearch.value.toLowerCase().trim(); if (searchTerm) { handleDeviceSearch(); } else { displayDevices(allDevices); } }); deviceList.appendChild(ungroupedHeader); } if (!collapsedSites.has('__ungrouped') || sortedSiteIds.length === 0) { ungrouped.forEach(device => { deviceList.appendChild(createDeviceItem(device, globalIndex++)); }); } } updateCookieProxyButtonStates(); } // 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 cookie device UUID field const uuid = device.guid || device.id || ''; cookieDeviceUUID.value = uuid; // Show device selection feedback with connection status if (uuid) { const isCookieActive = activeCookieProxyConnections.has(uuid); const cookieStatusText = isCookieActive ? ' (COOKIE PROXY ACTIVE)' : ''; showStatus(connectionStatus, `Selected device: ${device.name || 'Unnamed Device'} (UUID: ${uuid})${cookieStatusText}`, 'info'); } updateCookieProxyButtonStates(); } // Utility functions function showStatus(element, message, type) { element.textContent = message; element.className = `status-message ${type}`; element.style.display = 'block'; } function clearDeviceList() { deviceList.innerHTML = 'Connect to API to load devices
'; selectedDevice = null; } function escapeHtml(text) { if (typeof text !== 'string') return text; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // --- Self-Update Functions --- async function handleCheckForUpdates() { checkUpdateBtn.disabled = true; try { const result = await window.electronAPI.checkForUpdates(); if (!result.success) { showStatus(connectionStatus, result.message || 'Failed to check for updates', 'error'); return; } if (result.message === 'No releases available yet') { showStatus(connectionStatus, 'No releases available yet', 'info'); return; } if (result.updateAvailable) { showUpdateModal(result); } else { showStatus(connectionStatus, `You're on the latest version (v${result.currentVersion})`, 'success'); } } catch (error) { console.error('Check for updates error:', error); showStatus(connectionStatus, 'Could not check for updates. Check your internet connection.', 'error'); } finally { checkUpdateBtn.disabled = false; } } function showUpdateModal(updateInfo) { pendingUpdateInfo = updateInfo; updateModalMessage.textContent = `A new version is available: v${updateInfo.latestVersion} (current: v${updateInfo.currentVersion})`; updateModalNotes.textContent = updateInfo.releaseNotes || ''; // Reset progress state updateProgressContainer.style.display = 'none'; updateProgressFill.style.width = '0%'; updateProgressText.textContent = '0%'; // Reset button states updateInstallBtn.disabled = !updateInfo.downloadUrl; updateLaterBtn.disabled = false; updateInstallBtn.textContent = 'Install Update'; if (!updateInfo.downloadUrl) { updateModalMessage.textContent += '\n(No downloadable asset found for this release)'; } updateModalOverlay.style.display = 'flex'; } async function handleInstallUpdate() { if (!pendingUpdateInfo || !pendingUpdateInfo.downloadUrl) return; // Disable controls during download updateInstallBtn.disabled = true; updateInstallBtn.textContent = 'Downloading...'; updateLaterBtn.disabled = true; updateModalCloseBtn.style.display = 'none'; updateProgressContainer.style.display = 'flex'; try { const result = await window.electronAPI.downloadAndInstallUpdate({ downloadUrl: pendingUpdateInfo.downloadUrl }); if (result.success) { updateInstallBtn.textContent = 'Restarting...'; updateProgressFill.style.width = '100%'; updateProgressText.textContent = '100%'; } else { showStatus(connectionStatus, `Update failed: ${result.message}`, 'error'); closeUpdateModal(); } } catch (error) { console.error('Install update error:', error); showStatus(connectionStatus, 'Update failed. Please try again.', 'error'); closeUpdateModal(); } } function closeUpdateModal() { updateModalOverlay.style.display = 'none'; pendingUpdateInfo = null; updateProgressContainer.style.display = 'none'; updateProgressFill.style.width = '0%'; updateProgressText.textContent = '0%'; updateInstallBtn.textContent = 'Install Update'; updateInstallBtn.disabled = false; updateLaterBtn.disabled = false; updateModalCloseBtn.style.display = ''; } async function checkForUpdatesOnStartup() { try { const result = await window.electronAPI.checkForUpdates(); if (result.success && result.updateAvailable) { checkUpdateBtn.classList.add('update-available'); checkUpdateBtn.title = `Update available: v${result.latestVersion}`; showStatus(connectionStatus, `Update available: v${result.latestVersion}`, 'info'); // Store for quick modal open pendingUpdateInfo = result; } } catch (error) { // Silent fail on startup check console.log('Startup update check failed:', error.message); } } // 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 cookieKey.value = cookieValue; 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); // Listen for update download progress window.electronAPI.onUpdateDownloadProgress((data) => { updateProgressFill.style.width = `${data.percent}%`; updateProgressText.textContent = `${data.percent}%`; }); // Auto-check for updates after 2 seconds setTimeout(checkForUpdatesOnStartup, 2000); });