c3c3fc83e3
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>
765 lines
27 KiB
JavaScript
765 lines
27 KiB
JavaScript
// 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 = `
|
|
<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();
|
|
|
|
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;
|
|
}
|
|
|
|
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 = `
|
|
<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>
|
|
`;
|
|
|
|
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 = `
|
|
<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();
|
|
}
|
|
|
|
// 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 = '<p class="placeholder-text">Connect to API to load devices</p>';
|
|
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);
|
|
});
|