Add site-grouped device list in sidebar
Fetch device sites from /api/v1/deviceSites in parallel with devices, then group cameras by site with collapsible headers. Search now also matches site names. Falls back to flat list if sites API is unavailable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+178
-30
@@ -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 = `
|
||||
<div class="device-name">${escapeHtml(device.name || 'Unnamed Device')}</div>
|
||||
<div class="device-status-dot ${status.isOnline ? 'online' : 'offline'}"></div>
|
||||
`;
|
||||
|
||||
deviceItem.addEventListener('click', () => selectDevice(device, deviceItem));
|
||||
return deviceItem;
|
||||
}
|
||||
|
||||
// Display devices in the UI, grouped by site
|
||||
function displayDevices(devices) {
|
||||
clearDeviceList();
|
||||
|
||||
@@ -376,33 +458,99 @@ function displayDevices(devices) {
|
||||
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 hasSites = Object.keys(allSites).length > 0;
|
||||
|
||||
const deviceStatus = getDeviceStatus(device);
|
||||
const deviceId = device.guid || device.id;
|
||||
const isCookieProxyActive = activeCookieProxyConnections.has(deviceId);
|
||||
// If no sites were fetched, fall back to flat list
|
||||
if (!hasSites) {
|
||||
devices.forEach((device, index) => {
|
||||
deviceList.appendChild(createDeviceItem(device, index));
|
||||
});
|
||||
updateCookieProxyButtonStates();
|
||||
return;
|
||||
}
|
||||
|
||||
// Add cookie-proxy-active class if this device has an active cookie connection
|
||||
if (isCookieProxyActive) {
|
||||
deviceItem.classList.add('cookie-proxy-active');
|
||||
}
|
||||
const { groups, ungrouped } = groupDevicesBySite(devices);
|
||||
|
||||
deviceItem.innerHTML = `
|
||||
<div class="device-name">${escapeHtml(device.name || 'Unnamed Device')}</div>
|
||||
<div class="device-status-dot ${deviceStatus.isOnline ? 'online' : 'offline'}"></div>
|
||||
// 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 = `
|
||||
<span class="site-group-arrow">${isCollapsed ? '\u25B6' : '\u25BC'}</span>
|
||||
<span class="site-group-name">${escapeHtml(group.name)}</span>
|
||||
<span class="site-group-count">${group.devices.length}</span>
|
||||
`;
|
||||
|
||||
// Add click handler for device selection
|
||||
deviceItem.addEventListener('click', () => selectDevice(device, deviceItem));
|
||||
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(deviceItem);
|
||||
deviceList.appendChild(siteHeader);
|
||||
|
||||
// Device items (hidden if collapsed)
|
||||
if (!isCollapsed) {
|
||||
group.devices.forEach(device => {
|
||||
deviceList.appendChild(createDeviceItem(device, globalIndex++));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Update cookie proxy button states after displaying devices
|
||||
// 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 = `
|
||||
<span class="site-group-arrow">${collapsedSites.has('__ungrouped') ? '\u25B6' : '\u25BC'}</span>
|
||||
<span class="site-group-name">Ungrouped</span>
|
||||
<span class="site-group-count">${ungrouped.length}</span>
|
||||
`;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user