// MacroPad PWA Application (Execute-only) class MacroPadApp { constructor() { this.macros = {}; this.tabs = []; this.currentTab = 'All'; this.ws = null; this.wakeLock = null; // Relay mode detection this.relayMode = this.detectRelayMode(); this.sessionId = null; this.password = null; this.desktopConnected = true; this.wsAuthenticated = false; if (this.relayMode) { this.initRelayMode(); } this.init(); } detectRelayMode() { // Check if URL matches relay pattern: /sessionId/app or /sessionId const pathMatch = window.location.pathname.match(/^\/([a-zA-Z0-9]{4,12})(\/app)?$/); return pathMatch !== null; } initRelayMode() { // Extract session ID from URL const pathMatch = window.location.pathname.match(/^\/([a-zA-Z0-9]{4,12})/); if (pathMatch) { this.sessionId = pathMatch[1]; } // Get password from URL query param or sessionStorage const urlParams = new URLSearchParams(window.location.search); this.password = urlParams.get('auth') || sessionStorage.getItem(`macropad_${this.sessionId}`); if (this.password) { // Store password for future use sessionStorage.setItem(`macropad_${this.sessionId}`, this.password); // Clear from URL for security if (urlParams.has('auth')) { window.history.replaceState({}, '', window.location.pathname); } } console.log('Relay mode enabled, session:', this.sessionId); } getApiUrl(path) { if (this.relayMode && this.sessionId) { return `/${this.sessionId}${path}`; } return path; } getApiHeaders() { const headers = { 'Content-Type': 'application/json' }; if (this.relayMode && this.password) { headers['X-MacroPad-Password'] = this.password; } return headers; } async init() { await this.loadTabs(); await this.loadMacros(); this.setupWebSocket(); this.setupEventListeners(); this.setupWakeLock(); this.checkInstallPrompt(); } // API Methods async loadTabs() { try { const response = await fetch(this.getApiUrl('/api/tabs'), { headers: this.getApiHeaders() }); if (!response.ok) { if (response.status === 401) { this.handleAuthError(); return; } throw new Error('Failed to load tabs'); } const data = await response.json(); this.tabs = data.tabs || []; this.renderTabs(); } catch (error) { console.error('Error loading tabs:', error); this.showToast('Error loading tabs', 'error'); } } async loadMacros() { try { const path = this.currentTab === 'All' ? '/api/macros' : `/api/macros/${encodeURIComponent(this.currentTab)}`; const response = await fetch(this.getApiUrl(path), { headers: this.getApiHeaders() }); if (!response.ok) { if (response.status === 401) { this.handleAuthError(); return; } if (response.status === 503) { this.handleDesktopDisconnected(); return; } throw new Error('Failed to load macros'); } const data = await response.json(); this.macros = data.macros || {}; this.renderMacros(); } catch (error) { console.error('Error loading macros:', error); this.showToast('Error loading macros', 'error'); } } async executeMacro(macroId) { try { const card = document.querySelector(`[data-macro-id="${macroId}"]`); if (card) card.classList.add('executing'); const response = await fetch(this.getApiUrl('/api/execute'), { method: 'POST', headers: this.getApiHeaders(), body: JSON.stringify({ macro_id: macroId }) }); if (!response.ok) { if (response.status === 503) { this.handleDesktopDisconnected(); } throw new Error('Execution failed'); } setTimeout(() => { if (card) card.classList.remove('executing'); }, 300); } catch (error) { console.error('Error executing macro:', error); this.showToast('Error executing macro', 'error'); } } handleAuthError() { this.showToast('Authentication failed', 'error'); if (this.relayMode) { // Clear stored password and redirect to login sessionStorage.removeItem(`macropad_${this.sessionId}`); window.location.href = `/${this.sessionId}`; } } handleDesktopDisconnected() { this.desktopConnected = false; this.updateConnectionStatus(false, 'Desktop offline'); this.showToast('Desktop app is not connected', 'error'); } // WebSocket setupWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; let wsUrl; if (this.relayMode && this.sessionId) { wsUrl = `${protocol}//${window.location.host}/${this.sessionId}/ws`; } else { wsUrl = `${protocol}//${window.location.host}/ws`; } try { this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { if (!this.relayMode) { this.updateConnectionStatus(true); } // In relay mode, wait for auth before showing connected }; this.ws.onclose = () => { this.wsAuthenticated = false; this.updateConnectionStatus(false); setTimeout(() => this.setupWebSocket(), 3000); }; this.ws.onmessage = (event) => { const data = JSON.parse(event.data); this.handleWebSocketMessage(data); }; this.ws.onerror = () => { this.updateConnectionStatus(false); }; } catch (error) { console.error('WebSocket error:', error); } } handleWebSocketMessage(data) { switch (data.type) { // Relay-specific messages case 'auth_required': // Send authentication if (this.password) { this.ws.send(JSON.stringify({ type: 'auth', password: this.password })); } break; case 'auth_response': if (data.success) { this.wsAuthenticated = true; this.updateConnectionStatus(this.desktopConnected); } else { this.handleAuthError(); } break; case 'desktop_status': this.desktopConnected = data.status === 'connected'; this.updateConnectionStatus(this.desktopConnected); if (!this.desktopConnected) { this.showToast('Desktop disconnected', 'error'); } else { this.showToast('Desktop connected', 'success'); this.loadTabs(); this.loadMacros(); } break; // Standard MacroPad messages case 'macro_created': case 'macro_updated': case 'macro_deleted': this.loadTabs(); this.loadMacros(); break; case 'executed': const card = document.querySelector(`[data-macro-id="${data.macro_id}"]`); if (card) { card.classList.add('executing'); setTimeout(() => card.classList.remove('executing'), 300); } break; case 'pong': // Keep-alive response break; } } updateConnectionStatus(connected, customText = null) { const dot = document.querySelector('.status-dot'); const text = document.querySelector('.connection-status span'); if (dot) { dot.classList.toggle('connected', connected); } if (text) { if (customText) { text.textContent = customText; } else if (this.relayMode) { text.textContent = connected ? 'Connected (Relay)' : 'Disconnected'; } else { text.textContent = connected ? 'Connected' : 'Disconnected'; } } } // Rendering renderTabs() { const container = document.getElementById('tabs-container'); if (!container) return; container.innerHTML = this.tabs.map(tab => ` `).join(''); } renderMacros() { const container = document.getElementById('macro-grid'); if (!container) return; const macroEntries = Object.entries(this.macros); if (macroEntries.length === 0) { container.innerHTML = `
No macros found
Create macros in the desktop app