diff --git a/main.js b/main.js index 883936c..4bb5acd 100644 --- a/main.js +++ b/main.js @@ -218,6 +218,33 @@ ipcMain.handle('api-get-devices', async (event, { deploymentUrl, cookies }) => { } }); +ipcMain.handle('api-get-device-sites', async (event, { deploymentUrl, cookies }) => { + try { + const sitesUrl = `${deploymentUrl}/api/v1/deviceSites`; + + const axiosInstance = axios.create({ + timeout: 10000, + headers: { + 'Cookie': cookies ? cookies.join('; ') : '' + } + }); + + const response = await axiosInstance.get(sitesUrl); + + return { + success: true, + sites: response.data + }; + } catch (error) { + console.error('Get device sites error:', error); + return { + success: false, + sites: [], + message: error.response?.data?.message || error.message || 'Failed to get device sites' + }; + } +}); + ipcMain.handle('api-get-auth-info', async (event, { deploymentUrl, cookies }) => { try { const authUrl = `${deploymentUrl}/api/v1/auth`; diff --git a/preload.js b/preload.js index 0dad6df..bc3131e 100644 --- a/preload.js +++ b/preload.js @@ -4,6 +4,7 @@ const { contextBridge, ipcRenderer } = require('electron'); // the ipcRenderer without exposing the entire object contextBridge.exposeInMainWorld('electronAPI', { getDevices: (params) => ipcRenderer.invoke('api-get-devices', params), + getDeviceSites: (params) => ipcRenderer.invoke('api-get-device-sites', params), getAuthInfo: (params) => ipcRenderer.invoke('api-get-auth-info', params), // Camera proxy functionality diff --git a/renderer.js b/renderer.js index 07df1d0..2ee09b7 100644 --- a/renderer.js +++ b/renderer.js @@ -37,6 +37,8 @@ const updateLaterBtn = document.getElementById('updateLaterBtn'); 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 @@ -66,6 +68,8 @@ function handleDisconnect() { selectedDevice = null; activeCookieProxyConnections.clear(); allDevices = []; // Clear stored devices + allSites = {}; // Clear site data + collapsedSites.clear(); updateConnectionStatus(false); updateButtonStates(); @@ -218,6 +222,28 @@ function updateButtonStates() { 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) { @@ -229,14 +255,18 @@ async function handleGetDevices() { clearDeviceList(); try { - const result = await window.electronAPI.getDevices({ - deploymentUrl: sessionData.deploymentUrl, - cookies: sessionData.cookies - }); + // Fetch devices and sites in parallel + const [devicesResult] = await Promise.all([ + window.electronAPI.getDevices({ + deploymentUrl: sessionData.deploymentUrl, + cookies: sessionData.cookies + }), + fetchDeviceSites() + ]); - if (result.success) { + if (devicesResult.success) { // Filter devices to only show non-cloud cameras (localStorage = false) - const filteredDevices = result.devices.filter(device => { + 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) @@ -246,11 +276,15 @@ async function handleGetDevices() { return true; }); - const totalDevices = result.devices.length; + 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)`; } @@ -261,7 +295,7 @@ async function handleGetDevices() { allDevices = filteredDevices; displayDevices(filteredDevices); } else { - showStatus(deviceStatus, `Failed to get devices: ${result.message}`, 'error'); + showStatus(deviceStatus, `Failed to get devices: ${devicesResult.message}`, 'error'); } } catch (error) { console.error('Get devices error:', error); @@ -347,25 +381,73 @@ function handleDeviceSearch() { return; } - // Filter devices based on search term + // 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); + deviceIp.includes(searchTerm) || + siteName.includes(searchTerm); }); displayDevices(filteredDevices); } -// Display devices in the UI +// 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 = ` +