'use strict'; 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'); const disconnectBtn = document.getElementById('disconnectBtn'); const selectedDeviceId = document.getElementById('selectedDeviceId'); const startProxyBtn = document.getElementById('startProxyBtn'); const stopProxyBtn = document.getElementById('stopProxyBtn'); const checkUpdateBtn = document.getElementById('checkUpdateBtn'); const updateNotice = document.getElementById('updateNotice'); const updateMessage = document.getElementById('updateMessage'); const openReleasesBtn = document.getElementById('openReleasesBtn'); const dismissUpdateBtn = document.getElementById('dismissUpdateBtn'); const pairingState = document.getElementById('pairingState'); const pairingSecret = document.getElementById('pairingSecret'); const pairingSecretRow = document.getElementById('pairingSecretRow'); const rotatePairingBtn = document.getElementById('rotatePairingBtn'); const revokePairingBtn = document.getElementById('revokePairingBtn'); let connection = { connected: false, origin: null, activeProxies: [] }; let selectedDevice = null; let allDevices = []; let allSites = {}; let collapsedSites = new Set(); let loadingDevices = false; function showStatus(element, message, type) { element.textContent = message; element.className = `status-message ${type}`; element.style.display = 'block'; } function activeDeviceIds() { return new Set((connection.activeProxies || []).map((proxy) => proxy.deviceId)); } function renderConnectionState(state) { connection = { connected: state && state.connected === true, origin: state && typeof state.origin === 'string' ? state.origin : null, activeProxies: state && Array.isArray(state.activeProxies) ? state.activeProxies : [], }; const dot = statusIndicator.querySelector('.status-dot'); const text = statusIndicator.querySelector('.status-text'); dot.className = `status-dot ${connection.connected ? 'online' : 'offline'}`; text.textContent = connection.connected ? 'Connected' : 'Disconnected'; disconnectBtn.disabled = !connection.connected; updateProxyButtons(); } function updateProxyButtons() { const id = selectedDevice && (selectedDevice.guid || selectedDevice.id); const active = id ? activeDeviceIds().has(id.toLowerCase()) : false; startProxyBtn.disabled = !connection.connected || !id || active; stopProxyBtn.disabled = !id || !active; } function clearDeviceList() { deviceList.textContent = ''; const placeholder = document.createElement('p'); placeholder.className = 'placeholder-text'; placeholder.textContent = connection.connected ? 'Loading devices...' : 'Connect through the paired Chrome extension to load devices'; deviceList.appendChild(placeholder); selectedDevice = null; selectedDeviceId.value = ''; updateProxyButtons(); } function deviceStatusFor(device) { const raw = device && device.live && device.live.display_status !== undefined ? device.live.display_status : device && device.status !== undefined ? device.status : device && device.online; const value = String(raw ?? '').toLowerCase(); return ['green', 'online', 'live', 'connected', 'true'].includes(value); } function selectDevice(device, item) { const previous = deviceList.querySelector('.device-item.selected'); if (previous) previous.classList.remove('selected'); item.classList.add('selected'); selectedDevice = device; const id = device.guid || device.id || ''; selectedDeviceId.value = id; showStatus(connectionStatus, `Selected device: ${device.name || 'Unnamed Device'}`, 'info'); updateProxyButtons(); } function createDeviceItem(device) { const item = document.createElement('div'); const id = device.guid || device.id || ''; item.className = 'device-item'; item.dataset.deviceId = id; if (activeDeviceIds().has(String(id).toLowerCase())) item.classList.add('proxy-active'); const name = document.createElement('div'); name.className = 'device-name'; name.textContent = device.name || 'Unnamed Device'; const dot = document.createElement('div'); dot.className = `device-status-dot ${deviceStatusFor(device) ? 'online' : 'offline'}`; item.append(name, dot); item.addEventListener('click', () => selectDevice(device, item)); return item; } function groupDevicesBySite(devices) { const groups = new Map(); for (const device of devices) { const siteId = device.server_group_id; const siteName = siteId && allSites[siteId]; const key = siteName ? siteId : '__ungrouped'; if (!groups.has(key)) groups.set(key, { name: siteName || 'Ungrouped', devices: [] }); groups.get(key).devices.push(device); } return [...groups.entries()].sort((left, right) => left[1].name.localeCompare(right[1].name)); } function displayDevices(devices) { deviceList.textContent = ''; if (!devices.length) { const empty = document.createElement('p'); empty.className = 'no-devices'; empty.textContent = deviceSearch.value.trim() ? 'No devices match your search' : 'No local cameras found'; deviceList.appendChild(empty); return; } const grouped = Object.keys(allSites).length > 0; if (!grouped) { for (const device of devices) deviceList.appendChild(createDeviceItem(device)); return; } for (const [siteId, group] of groupDevicesBySite(devices)) { const header = document.createElement('div'); const collapsed = collapsedSites.has(siteId); header.className = `site-group-header${collapsed ? ' collapsed' : ''}`; const arrow = document.createElement('span'); arrow.className = 'site-group-arrow'; arrow.textContent = collapsed ? '\u25B6' : '\u25BC'; const name = document.createElement('span'); name.className = 'site-group-name'; name.textContent = group.name; const count = document.createElement('span'); count.className = 'site-group-count'; count.textContent = String(group.devices.length); header.append(arrow, name, count); header.addEventListener('click', () => { if (collapsedSites.has(siteId)) collapsedSites.delete(siteId); else collapsedSites.add(siteId); filterDevices(); }); deviceList.appendChild(header); if (!collapsed) for (const device of group.devices) deviceList.appendChild(createDeviceItem(device)); } } function filterDevices() { const query = deviceSearch.value.toLowerCase().trim(); if (!query) { displayDevices(allDevices); return; } displayDevices(allDevices.filter((device) => { const values = [ device.name, device.guid, device.id, device.type, device.model, device.ipAddress, device.server_group_id && allSites[device.server_group_id], ]; return values.some((value) => String(value || '').toLowerCase().includes(query)); })); } async function loadDevices() { if (!connection.connected || loadingDevices) return; loadingDevices = true; showStatus(deviceStatus, 'Fetching devices...', 'info'); try { const [devicesResult, sitesResult] = await Promise.all([ window.electronAPI.getDevices(), window.electronAPI.getDeviceSites(), ]); allSites = {}; if (sitesResult.success && Array.isArray(sitesResult.sites)) { for (const site of sitesResult.sites) if (site.id) allSites[site.id] = site.name || 'Unnamed Site'; } if (!devicesResult.success) { allDevices = []; showStatus(deviceStatus, devicesResult.message || 'Failed to load devices', 'error'); clearDeviceList(); return; } allDevices = devicesResult.devices.filter((device) => !device.capabilities || device.capabilities.localStorage === undefined || device.capabilities.localStorage === false ); showStatus(deviceStatus, `Found ${allDevices.length} local camera${allDevices.length === 1 ? '' : 's'}`, 'success'); filterDevices(); } catch { showStatus(deviceStatus, 'Could not load devices.', 'error'); } finally { loadingDevices = false; } } async function handleDisconnect() { const state = await window.electronAPI.disconnect(); renderConnectionState(state); allDevices = []; allSites = {}; collapsedSites.clear(); clearDeviceList(); showStatus(connectionStatus, 'Disconnected from Alta.', 'info'); } async function handleStartProxy() { const id = selectedDevice && (selectedDevice.guid || selectedDevice.id); if (!id) return; startProxyBtn.disabled = true; showStatus(connectionStatus, `Starting proxy for ${selectedDevice.name || 'selected device'}...`, 'info'); const result = await window.electronAPI.launchProxy(id); if (result.success) { connection = await window.electronAPI.getConnectionState(); renderConnectionState(connection); filterDevices(); showStatus(connectionStatus, 'Camera proxy started.', 'success'); } else { showStatus(connectionStatus, result.message || 'Failed to start camera proxy.', 'error'); updateProxyButtons(); } } async function handleStopProxy() { const id = selectedDevice && (selectedDevice.guid || selectedDevice.id); if (!id) return; stopProxyBtn.disabled = true; const result = await window.electronAPI.stopProxy(id); connection = await window.electronAPI.getConnectionState(); renderConnectionState(connection); filterDevices(); showStatus(connectionStatus, result.success ? 'Camera proxy stopped.' : result.message || 'Failed to stop camera proxy.', result.success ? 'success' : 'error'); } async function checkForUpdates(showCurrent = true) { checkUpdateBtn.disabled = true; try { const result = await window.electronAPI.checkForUpdates(); if (!result.success) { if (showCurrent) showStatus(connectionStatus, result.message, 'error'); return; } if (result.status === 'update-available') { updateMessage.textContent = `${result.releaseName || `Version ${result.latestVersion}`} is available. APT will not download or run updates; review it on GitPeji.`; updateNotice.style.display = 'flex'; checkUpdateBtn.classList.add('update-available'); } else if (showCurrent) { const message = result.status === 'no-release' ? 'No published release is available on GitPeji.' : `You are on the latest version (v${result.currentVersion}).`; showStatus(connectionStatus, message, 'success'); } } finally { checkUpdateBtn.disabled = false; } } function renderPairingStatus(result) { pairingState.textContent = result.paired ? 'Paired' : 'Revoked / not paired'; pairingState.className = result.paired ? 'paired' : 'unpaired'; revokePairingBtn.disabled = !result.paired; if (result.secret) { pairingSecret.value = result.secret; pairingSecretRow.style.display = 'block'; showStatus(connectionStatus, 'One-time bridge pairing secret generated. Paste it into the extension now; it will not be shown again.', 'info'); } else { pairingSecret.value = ''; pairingSecretRow.style.display = 'none'; } } async function initialize() { renderConnectionState(await window.electronAPI.getConnectionState()); renderPairingStatus(await window.electronAPI.getPairingStatus()); if (connection.connected) await loadDevices(); window.electronAPI.onConnectionStateChanged(async (state) => { renderConnectionState(state); if (state.connected) { allDevices = []; allSites = {}; clearDeviceList(); showStatus(connectionStatus, `Connected to ${state.origin}`, 'success'); await loadDevices(); } }); setTimeout(() => checkForUpdates(false), 2000); } disconnectBtn.addEventListener('click', handleDisconnect); startProxyBtn.addEventListener('click', handleStartProxy); stopProxyBtn.addEventListener('click', handleStopProxy); deviceSearch.addEventListener('input', filterDevices); checkUpdateBtn.addEventListener('click', () => checkForUpdates(true)); openReleasesBtn.addEventListener('click', () => window.electronAPI.openFixedReleasesPage()); dismissUpdateBtn.addEventListener('click', () => { updateNotice.style.display = 'none'; }); rotatePairingBtn.addEventListener('click', async () => renderPairingStatus(await window.electronAPI.rotatePairing())); revokePairingBtn.addEventListener('click', async () => renderPairingStatus(await window.electronAPI.revokePairing())); document.addEventListener('DOMContentLoaded', () => { initialize().catch(() => showStatus(connectionStatus, 'Application initialization failed.', 'error')); });