UNPKG

peerpigeon

Version:

WebRTC-based peer-to-peer mesh networking library with intelligent routing and signaling server

1,439 lines (1,207 loc) β€’ 124 kB
// Updated: 2025-07-04 - Fixed getConnectionStatus method name export class PeerPigeonUI { constructor(mesh) { this.mesh = mesh; this.lastCleanupTime = 0; // Track when we last did cleanup this.setupEventListeners(); this.bindDOMEvents(); this.initializeMedia(); } setupEventListeners() { this.mesh.addEventListener('statusChanged', (data) => { this.handleStatusChange(data); }); this.mesh.addEventListener('peerDiscovered', (_data) => { this.updateDiscoveredPeers(); }); this.mesh.addEventListener('peerConnected', (_data) => { this.updateUI(); this.updateDiscoveredPeers(); }); this.mesh.addEventListener('peerDisconnected', (data) => { this.addMessage('System', `Peer ${data.peerId.substring(0, 8)}... disconnected (${data.reason})`); this.updateUI(); this.updateDiscoveredPeers(); }); this.mesh.addEventListener('messageReceived', (data) => { // Only show messages from other peers, not self if (data.from !== this.mesh.peerId) { if (data.direct) { this.addMessage(`${data.from.substring(0, 8)}...`, `(DM) ${data.content}`, 'dm'); } else { this.addMessage(`${data.from.substring(0, 8)}...`, data.content); } } }); this.mesh.addEventListener('peerEvicted', (_data) => { this.updateUI(); this.updateDiscoveredPeers(); }); this.mesh.addEventListener('peersUpdated', () => { this.updateDiscoveredPeers(); }); this.mesh.addEventListener('connectionStats', (_stats) => { // Handle connection stats if needed }); // DHT event listeners this.mesh.addEventListener('dhtValueChanged', (data) => { const { key, newValue, timestamp } = data; this.addDHTLogEntry(`πŸ”” Value Changed: ${key} = ${JSON.stringify(newValue)} (timestamp: ${timestamp})`); }); // Media event listeners this.mesh.addEventListener('localStreamStarted', (data) => { this.handleLocalStreamStarted(data); }); this.mesh.addEventListener('localStreamStopped', () => { this.handleLocalStreamStopped(); }); this.mesh.addEventListener('mediaError', (data) => { this.addMessage('System', `Media error: ${data.error.message}`, 'error'); }); // Listen for remote streams this.mesh.addEventListener('remoteStream', (data) => { this.handleRemoteStream(data); }); // Crypto event listeners this.mesh.addEventListener('cryptoReady', (_data) => { this.addMessage('System', 'πŸ” Crypto initialized successfully', 'success'); this.updateCryptoStatus(); }); this.mesh.addEventListener('cryptoError', (data) => { this.addMessage('System', `πŸ” Crypto error: ${data.error}`, 'error'); this.addCryptoTestResult(`❌ Crypto Error: ${data.error}`, 'error'); }); this.mesh.addEventListener('peerKeyAdded', (data) => { this.addMessage('System', `πŸ” Public key received from ${data.peerId.substring(0, 8)}...`); this.updateCryptoStatus(); }); this.mesh.addEventListener('userAuthenticated', (data) => { this.addMessage('System', `πŸ” Authenticated as ${data.alias}`, 'success'); this.updateCryptoStatus(); }); } handleStatusChange(data) { switch (data.type) { case 'initialized': this.updateUI(); break; case 'connecting': this.addMessage('System', 'Connecting to signaling server...'); this.updateUI(); break; case 'connected': this.addMessage('System', 'Connected to signaling server'); this.updateUI(); break; case 'disconnected': this.addMessage('System', 'Disconnected from signaling server'); this.updateUI(); this.updateDiscoveredPeers(); break; case 'error': this.addMessage('System', data.message, 'error'); this.updateUI(); break; case 'warning': this.addMessage('System', data.message, 'warning'); break; case 'info': this.addMessage('System', data.message); break; case 'urlLoaded': document.getElementById('signaling-url').value = data.signalingUrl; this.addMessage('System', `API Gateway URL loaded: ${data.signalingUrl}`); break; case 'setting': { const settingName = data.setting === 'autoDiscovery' ? 'Auto discovery' : data.setting === 'evictionStrategy' ? 'Smart eviction strategy' : data.setting === 'xorRouting' ? 'XOR-based routing' : data.setting === 'connectionType' ? 'Connection type' : data.setting; if (data.setting === 'connectionType') { this.addMessage('System', `${settingName} set to: ${data.value}`); } else { this.addMessage('System', `${settingName} ${data.value ? 'enabled' : 'disabled'}`); } break; } } } bindDOMEvents() { // Connection controls document.getElementById('connect-btn').addEventListener('click', () => { this.handleConnect(); }); document.getElementById('disconnect-btn').addEventListener('click', () => { this.handleDisconnect(); }); document.getElementById('cleanup-btn').addEventListener('click', () => { this.handleCleanup(); }); document.getElementById('health-check-btn').addEventListener('click', () => { this.handleHealthCheck(); }); document.getElementById('refresh-btn').addEventListener('click', () => { window.location.reload(); }); // Messaging document.getElementById('send-message-btn').addEventListener('click', () => { this.sendMessage(); }); document.getElementById('message-input').addEventListener('keypress', (e) => { if (e.key === 'Enter') { this.sendMessage(); } }); // Manual connection document.getElementById('connect-peer-btn').addEventListener('click', () => { this.connectToPeer(); }); document.getElementById('target-peer').addEventListener('keypress', (e) => { if (e.key === 'Enter') { this.connectToPeer(); } }); // Settings document.getElementById('min-peers').addEventListener('change', (e) => { this.mesh.setMinPeers(parseInt(e.target.value)); }); document.getElementById('max-peers').addEventListener('change', (e) => { this.mesh.setMaxPeers(parseInt(e.target.value)); }); document.getElementById('auto-discovery-toggle').addEventListener('change', (e) => { this.mesh.setAutoDiscovery(e.target.checked); }); document.getElementById('eviction-strategy-toggle').addEventListener('change', (e) => { this.mesh.setEvictionStrategy(e.target.checked); }); document.getElementById('xor-routing-toggle').addEventListener('change', (e) => { this.mesh.setXorRouting(e.target.checked); }); // DHT controls this.setupDHTControls(); // Storage controls this.setupStorageControls(); // Crypto controls this.setupCryptoControls(); // Collapsible sections this.setupCollapsibleSections(); // Media controls this.setupMediaControls(); } setupDHTControls() { // DHT input fields and controls const dhtKey = document.getElementById('dht-key'); const dhtValue = document.getElementById('dht-value'); const dhtGetKey = document.getElementById('dht-get-key'); // Separate key field for retrieval const dhtSubscribeKey = document.getElementById('dht-subscribe-key'); // Separate key field for subscription const dhtPutBtn = document.getElementById('dht-put-btn'); const dhtUpdateBtn = document.getElementById('dht-update-btn'); const dhtGetBtn = document.getElementById('dht-get-btn'); const dhtSubscribeBtn = document.getElementById('dht-subscribe-btn'); const dhtUnsubscribeBtn = document.getElementById('dht-unsubscribe-btn'); const dhtEnableTtl = document.getElementById('dht-enable-ttl'); const dhtTtlGroup = document.getElementById('dht-ttl-group'); const dhtTtl = document.getElementById('dht-ttl'); // Debug: Check if elements are found console.log('DHT Controls setup - Elements found:', { dhtKey: !!dhtKey, dhtValue: !!dhtValue, dhtGetKey: !!dhtGetKey, dhtSubscribeKey: !!dhtSubscribeKey, dhtPutBtn: !!dhtPutBtn, dhtUpdateBtn: !!dhtUpdateBtn, dhtGetBtn: !!dhtGetBtn, dhtSubscribeBtn: !!dhtSubscribeBtn, dhtUnsubscribeBtn: !!dhtUnsubscribeBtn }); if (!dhtKey) { console.error('DHT key input element not found!'); this.addDHTLogEntry('❌ Error: DHT key input field not found in DOM'); } if (!dhtValue) { console.error('DHT value input element not found!'); this.addDHTLogEntry('❌ Error: DHT value input field not found in DOM'); } if (!dhtGetKey) { console.error('DHT get-key input element not found!'); this.addDHTLogEntry('❌ Error: DHT get-key input field not found in DOM'); } // Toggle TTL input visibility if (dhtEnableTtl && dhtTtlGroup) { dhtEnableTtl.addEventListener('change', (e) => { dhtTtlGroup.style.display = e.target.checked ? 'block' : 'none'; }); } // Store in DHT if (dhtPutBtn) { dhtPutBtn.addEventListener('click', async () => { const key = dhtKey?.value?.trim(); const value = dhtValue?.value?.trim(); // Debug logging console.log('DHT PUT - Elements found:', { dhtKey: !!dhtKey, dhtValue: !!dhtValue, keyValue: key, valueValue: value }); if (!key || !value) { this.addDHTLogEntry('❌ Error: Both key and value are required'); this.addDHTLogEntry(` Debug: key="${key}", value="${value}"`); return; } if (!this.mesh.isDHTEnabled()) { this.addDHTLogEntry('❌ Error: WebDHT is disabled'); return; } try { let parsedValue; try { parsedValue = JSON.parse(value); } catch { parsedValue = value; // Use as string if not valid JSON } const options = {}; if (dhtEnableTtl?.checked && dhtTtl?.value) { options.ttl = parseInt(dhtTtl.value) * 1000; // Convert to milliseconds } const success = await this.mesh.dhtPut(key, parsedValue, options); if (success) { this.addDHTLogEntry(`βœ… Stored: ${key} = ${JSON.stringify(parsedValue)}`); if (options.ttl) { this.addDHTLogEntry(` TTL: ${options.ttl / 1000} seconds`); } this.addDHTLogEntry(` πŸ† This peer is now an ORIGINAL STORING PEER for "${key}"`); // Auto-subscribe to the key we just stored (essential for original storing peers) try { await this.mesh.dhtSubscribe(key); this.addDHTLogEntry(`πŸ”” Auto-subscribed as original storing peer: ${key}`); this.addDHTLogEntry(' πŸ“‘ Will receive and relay all future updates for this key'); } catch (subscribeError) { this.addDHTLogEntry(`⚠️ Failed to auto-subscribe to ${key}: ${subscribeError.message}`); } } else { this.addDHTLogEntry(`❌ Failed to store: ${key}`); } } catch (error) { this.addDHTLogEntry(`❌ Error storing ${key}: ${error.message}`); } }); } // Update in DHT if (dhtUpdateBtn) { dhtUpdateBtn.addEventListener('click', async () => { const key = dhtKey?.value?.trim(); const value = dhtValue?.value?.trim(); // Debug logging console.log('DHT UPDATE - Elements found:', { dhtKey: !!dhtKey, dhtValue: !!dhtValue, keyValue: key, valueValue: value }); if (!key || !value) { this.addDHTLogEntry('❌ Error: Both key and value are required'); this.addDHTLogEntry(` Debug: key="${key}", value="${value}"`); return; } if (!this.mesh.isDHTEnabled()) { this.addDHTLogEntry('❌ Error: WebDHT is disabled'); return; } try { let parsedValue; try { parsedValue = JSON.parse(value); } catch { parsedValue = value; // Use as string if not valid JSON } const options = {}; if (dhtEnableTtl?.checked && dhtTtl?.value) { options.ttl = parseInt(dhtTtl.value) * 1000; // Convert to milliseconds } const success = await this.mesh.dhtUpdate(key, parsedValue, options); if (success) { this.addDHTLogEntry(`πŸ”„ Updated: ${key} = ${JSON.stringify(parsedValue)}`); if (options.ttl) { this.addDHTLogEntry(` TTL: ${options.ttl / 1000} seconds`); } this.addDHTLogEntry(` πŸ† This peer is now an ORIGINAL STORING PEER for "${key}"`); // Auto-subscribe to the key we just updated (essential for original storing peers) try { await this.mesh.dhtSubscribe(key); this.addDHTLogEntry(`πŸ”” Auto-subscribed as original storing peer: ${key}`); this.addDHTLogEntry(' πŸ“‘ Will receive and relay all future updates for this key'); } catch (subscribeError) { this.addDHTLogEntry(`⚠️ Failed to auto-subscribe to ${key}: ${subscribeError.message}`); } } else { this.addDHTLogEntry(`❌ Failed to update: ${key}`); } } catch (error) { this.addDHTLogEntry(`❌ Error updating ${key}: ${error.message}`); } }); } // Get from DHT if (dhtGetBtn) { dhtGetBtn.addEventListener('click', async () => { const key = dhtGetKey?.value?.trim(); // Fixed: Use dhtGetKey instead of dhtKey // Debug logging console.log('DHT GET - Elements found:', { dhtGetKey: !!dhtGetKey, keyValue: key }); if (!key) { this.addDHTLogEntry('❌ Error: Key is required'); this.addDHTLogEntry(` Debug: key="${key}"`); return; } if (!this.mesh.isDHTEnabled()) { this.addDHTLogEntry('❌ Error: WebDHT is disabled'); return; } try { this.addDHTLogEntry(`πŸ” Fetching from ORIGINAL STORING PEERS: ${key}`); this.addDHTLogEntry(' πŸ“‘ Bypassing local cache to ensure fresh data'); // Force fresh retrieval from original storing peers const value = await this.mesh.dhtGet(key, { forceRefresh: true }); if (value !== null) { this.addDHTLogEntry(`πŸ“₯ Retrieved from original peers: ${key} = ${JSON.stringify(value)}`); this.addDHTLogEntry(' βœ… Data is FRESH from authoritative source'); if (dhtValue) { dhtValue.value = typeof value === 'string' ? value : JSON.stringify(value); } // Auto-subscribe to the key we just retrieved for future updates try { await this.mesh.dhtSubscribe(key); this.addDHTLogEntry(`πŸ”” Auto-subscribed for future updates: ${key}`); this.addDHTLogEntry(' πŸ“‘ Will receive notifications when original peers update this key'); } catch (subscribeError) { this.addDHTLogEntry(`⚠️ Failed to auto-subscribe to ${key}: ${subscribeError.message}`); } } else { this.addDHTLogEntry(`❌ Not found on original storing peers: ${key}`); this.addDHTLogEntry(' πŸ’‘ Key may not exist or all original storing peers are offline'); } } catch (error) { this.addDHTLogEntry(`❌ Error retrieving ${key}: ${error.message}`); } }); } // Subscribe to DHT key if (dhtSubscribeBtn) { dhtSubscribeBtn.addEventListener('click', async () => { // Use the dedicated subscription key field const key = dhtSubscribeKey?.value?.trim(); if (!key) { this.addDHTLogEntry('❌ Error: Key is required for subscription'); return; } if (!this.mesh.isDHTEnabled()) { this.addDHTLogEntry('❌ Error: WebDHT is disabled'); return; } try { this.addDHTLogEntry(`πŸ”” Explicitly subscribing to key: ${key}`); this.addDHTLogEntry(' πŸ“‘ Will receive notifications when original storing peers update this key'); const value = await this.mesh.dhtSubscribe(key); this.addDHTLogEntry(`βœ… Subscribed to: ${key}`); if (value !== null) { this.addDHTLogEntry(` Current value from original storing peers: ${JSON.stringify(value)}`); } else { this.addDHTLogEntry(' No current value found on original storing peers'); } // Clear the subscription key field after successful subscription if (dhtSubscribeKey) { dhtSubscribeKey.value = ''; } } catch (error) { this.addDHTLogEntry(`❌ Error subscribing to ${key}: ${error.message}`); } }); } // Unsubscribe from DHT key if (dhtUnsubscribeBtn) { dhtUnsubscribeBtn.addEventListener('click', async () => { // Use the dedicated subscription key field const key = dhtSubscribeKey?.value?.trim(); if (!key) { this.addDHTLogEntry('❌ Error: Key is required for unsubscription'); return; } if (!this.mesh.isDHTEnabled()) { this.addDHTLogEntry('❌ Error: WebDHT is disabled'); return; } try { await this.mesh.dhtUnsubscribe(key); this.addDHTLogEntry(`πŸ”• Unsubscribed from: ${key}`); // Clear the subscription key field after successful unsubscription if (dhtSubscribeKey) { dhtSubscribeKey.value = ''; } } catch (error) { this.addDHTLogEntry(`❌ Error unsubscribing from ${key}: ${error.message}`); } }); } } setupStorageControls() { // Ensure storage section starts collapsed with robust hiding const storageSection = document.querySelector('.storage'); const storageToggle = document.getElementById('storage-toggle'); const storageContent = document.getElementById('storage-content'); if (storageSection && storageToggle && storageContent) { // Force initial collapsed state with multiple approaches storageSection.setAttribute('aria-expanded', 'false'); storageToggle.setAttribute('aria-expanded', 'false'); // Use multiple ways to hide the content storageContent.style.display = 'none'; storageContent.style.visibility = 'hidden'; storageContent.style.maxHeight = '0'; storageContent.style.overflow = 'hidden'; storageContent.classList.add('collapsed'); // Ensure the toggle button shows collapsed state if (storageToggle.textContent && !storageToggle.textContent.includes('β–Ά')) { storageToggle.textContent = storageToggle.textContent.replace('β–Ό', 'β–Ά'); } } // Enable/Disable Storage const storageEnableBtn = document.getElementById('storage-enable-btn'); const storageDisableBtn = document.getElementById('storage-disable-btn'); const storageClearBtn = document.getElementById('storage-clear-btn'); if (storageEnableBtn) { storageEnableBtn.addEventListener('click', async () => { try { if (!this.mesh.distributedStorage) { this.addStorageLogEntry('❌ Error: Distributed storage not available'); return; } await this.mesh.distributedStorage.enable(); this.addStorageLogEntry('βœ… Distributed storage enabled'); this.updateStorageStatus(); } catch (error) { this.addStorageLogEntry(`❌ Error enabling storage: ${error.message}`); } }); } if (storageDisableBtn) { storageDisableBtn.addEventListener('click', async () => { try { if (!this.mesh.distributedStorage) { this.addStorageLogEntry('❌ Error: Distributed storage not available'); return; } await this.mesh.distributedStorage.disable(); this.addStorageLogEntry('⚠️ Distributed storage disabled'); this.updateStorageStatus(); } catch (error) { this.addStorageLogEntry(`❌ Error disabling storage: ${error.message}`); } }); } if (storageClearBtn) { storageClearBtn.addEventListener('click', async () => { if (!confirm('Are you sure you want to clear all stored data? This cannot be undone.')) { return; } try { if (!this.mesh.distributedStorage) { this.addStorageLogEntry('❌ Error: Distributed storage not available'); return; } await this.mesh.distributedStorage.clear(); this.addStorageLogEntry('πŸ—‘οΈ All stored data cleared'); this.updateStorageStatus(); } catch (error) { this.addStorageLogEntry(`❌ Error clearing storage: ${error.message}`); } }); } // Store Data const storageStoreBtn = document.getElementById('storage-store-btn'); const storageUpdateBtn = document.getElementById('storage-update-btn'); if (storageStoreBtn) { storageStoreBtn.addEventListener('click', async () => { await this.handleStorageStore(false); }); } if (storageUpdateBtn) { storageUpdateBtn.addEventListener('click', async () => { await this.handleStorageStore(true); }); } // Retrieve Data const storageGetBtn = document.getElementById('storage-get-btn'); const storageDeleteBtn = document.getElementById('storage-delete-btn'); if (storageGetBtn) { storageGetBtn.addEventListener('click', async () => { const key = document.getElementById('storage-get-key')?.value?.trim(); if (!key) { this.addStorageLogEntry('❌ Error: Key is required'); return; } try { if (!this.mesh.distributedStorage) { this.addStorageLogEntry('❌ Error: Distributed storage not available'); return; } const result = await this.mesh.distributedStorage.retrieve(key); if (result !== null) { this.addStorageLogEntry(`πŸ“‹ Retrieved: ${key} = ${JSON.stringify(result)}`); // Get key info for metadata display try { const keyInfo = await this.mesh.distributedStorage.getKeyInfo(key); if (keyInfo) { this.addStorageLogEntry(` Metadata: public=${keyInfo.isPublic}, immutable=${keyInfo.isImmutable}, owner=${keyInfo.owner?.substring(0, 8)}...`); this.addStorageLogEntry(` Created: ${new Date(keyInfo.createdAt).toLocaleString()}`); } } catch (metaError) { // Metadata display is optional, don't fail the retrieval console.warn('Could not get metadata for display:', metaError); } } else { this.addStorageLogEntry(`❌ Key not found or access denied: ${key}`); } } catch (error) { this.addStorageLogEntry(`❌ Error retrieving ${key}: ${error.message}`); } }); } if (storageDeleteBtn) { storageDeleteBtn.addEventListener('click', async () => { const key = document.getElementById('storage-get-key')?.value?.trim(); if (!key) { this.addStorageLogEntry('❌ Error: Key is required'); return; } if (!confirm(`Are you sure you want to delete "${key}"? This cannot be undone.`)) { return; } try { if (!this.mesh.distributedStorage) { this.addStorageLogEntry('❌ Error: Distributed storage not available'); return; } const success = await this.mesh.distributedStorage.delete(key); if (success) { this.addStorageLogEntry(`πŸ—‘οΈ Deleted: ${key}`); this.updateStorageStatus(); } else { this.addStorageLogEntry(`❌ Failed to delete: ${key}`); } } catch (error) { this.addStorageLogEntry(`❌ Error deleting ${key}: ${error.message}`); } }); } // Access Control const storageGrantAccessBtn = document.getElementById('storage-grant-access-btn'); const storageRevokeAccessBtn = document.getElementById('storage-revoke-access-btn'); if (storageGrantAccessBtn) { storageGrantAccessBtn.addEventListener('click', async () => { const key = document.getElementById('storage-access-key')?.value?.trim(); const peerId = document.getElementById('storage-access-peer')?.value?.trim(); const level = document.getElementById('storage-access-level')?.value; if (!key || !peerId) { this.addStorageLogEntry('❌ Error: Key and peer ID are required'); return; } try { if (!this.mesh.distributedStorage) { this.addStorageLogEntry('❌ Error: Distributed storage not available'); return; } const success = await this.mesh.distributedStorage.grantAccess(key, peerId, level); if (success) { this.addStorageLogEntry(`βœ… Granted ${level} access to ${peerId.substring(0, 8)}... for key: ${key}`); } else { this.addStorageLogEntry('❌ Failed to grant access'); } } catch (error) { this.addStorageLogEntry(`❌ Error granting access: ${error.message}`); } }); } if (storageRevokeAccessBtn) { storageRevokeAccessBtn.addEventListener('click', async () => { const key = document.getElementById('storage-revoke-key')?.value?.trim(); const peerId = document.getElementById('storage-revoke-peer')?.value?.trim(); if (!key || !peerId) { this.addStorageLogEntry('❌ Error: Key and peer ID are required'); return; } try { if (!this.mesh.distributedStorage) { this.addStorageLogEntry('❌ Error: Distributed storage not available'); return; } const success = await this.mesh.distributedStorage.revokeAccess(key, peerId); if (success) { this.addStorageLogEntry(`βœ… Revoked access from ${peerId.substring(0, 8)}... for key: ${key}`); } else { this.addStorageLogEntry('❌ Failed to revoke access'); } } catch (error) { this.addStorageLogEntry(`❌ Error revoking access: ${error.message}`); } }); } // Bulk Operations const storageListBtn = document.getElementById('storage-list-btn'); const storageBulkDeleteBtn = document.getElementById('storage-bulk-delete-btn'); if (storageListBtn) { storageListBtn.addEventListener('click', async () => { const prefix = document.getElementById('storage-prefix')?.value?.trim() || ''; try { if (!this.mesh.distributedStorage) { this.addStorageLogEntry('❌ Error: Distributed storage not available'); return; } const keys = await this.mesh.distributedStorage.listKeys(prefix); if (keys.length > 0) { this.addStorageLogEntry(`πŸ“‹ Found ${keys.length} keys with prefix "${prefix}":`); keys.forEach(key => { this.addStorageLogEntry(` β€’ ${key}`); }); } else { this.addStorageLogEntry(`❌ No keys found with prefix "${prefix}"`); } } catch (error) { this.addStorageLogEntry(`❌ Error listing keys: ${error.message}`); } }); } if (storageBulkDeleteBtn) { storageBulkDeleteBtn.addEventListener('click', async () => { const prefix = document.getElementById('storage-prefix')?.value?.trim(); if (!prefix) { this.addStorageLogEntry('❌ Error: Prefix is required for bulk delete'); return; } if (!confirm(`Are you sure you want to delete all keys with prefix "${prefix}"? This cannot be undone.`)) { return; } try { if (!this.mesh.distributedStorage) { this.addStorageLogEntry('❌ Error: Distributed storage not available'); return; } const count = await this.mesh.distributedStorage.bulkDelete(prefix); this.addStorageLogEntry(`πŸ—‘οΈ Bulk deleted ${count} keys with prefix "${prefix}"`); this.updateStorageStatus(); } catch (error) { this.addStorageLogEntry(`❌ Error bulk deleting: ${error.message}`); } }); } // Search const storageSearchBtn = document.getElementById('storage-search-btn'); if (storageSearchBtn) { storageSearchBtn.addEventListener('click', async () => { const query = document.getElementById('storage-search-query')?.value?.trim(); const type = document.getElementById('storage-search-type')?.value || 'key'; if (!query) { this.addStorageLogEntry('❌ Error: Search query is required'); return; } try { if (!this.mesh.distributedStorage) { this.addStorageLogEntry('❌ Error: Distributed storage not available'); return; } const results = await this.mesh.distributedStorage.search(query, type); if (results.length > 0) { this.addStorageLogEntry(`πŸ” Found ${results.length} results for "${query}" in ${type}:`); results.forEach(result => { this.addStorageLogEntry(` β€’ ${result.key}: ${JSON.stringify(result.value).substring(0, 100)}...`); }); } else { this.addStorageLogEntry(`❌ No results found for "${query}" in ${type}`); } } catch (error) { this.addStorageLogEntry(`❌ Error searching: ${error.message}`); } }); } // Backup/Restore const storageBackupBtn = document.getElementById('storage-backup-btn'); const storageRestoreBtn = document.getElementById('storage-restore-btn'); const storageRestoreFile = document.getElementById('storage-restore-file'); if (storageBackupBtn) { storageBackupBtn.addEventListener('click', async () => { try { if (!this.mesh.distributedStorage) { this.addStorageLogEntry('❌ Error: Distributed storage not available'); return; } const backup = await this.mesh.distributedStorage.backup(); const blob = new Blob([JSON.stringify(backup, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `peerpigeon-storage-backup-${new Date().toISOString().replace(/[:.]/g, '-')}.json`; a.click(); URL.revokeObjectURL(url); this.addStorageLogEntry(`πŸ’Ύ Backup created with ${backup.items.length} items`); } catch (error) { this.addStorageLogEntry(`❌ Error creating backup: ${error.message}`); } }); } if (storageRestoreBtn) { storageRestoreBtn.addEventListener('click', () => { storageRestoreFile?.click(); }); } if (storageRestoreFile) { storageRestoreFile.addEventListener('change', async (e) => { const file = e.target.files[0]; if (!file) return; try { const text = await file.text(); const backup = JSON.parse(text); if (!this.mesh.distributedStorage) { this.addStorageLogEntry('❌ Error: Distributed storage not available'); return; } const result = await this.mesh.distributedStorage.restore(backup); this.addStorageLogEntry(`πŸ“₯ Restored ${result.restored} items (${result.failed} failed)`); this.updateStorageStatus(); } catch (error) { this.addStorageLogEntry(`❌ Error restoring backup: ${error.message}`); } // Clear the file input e.target.value = ''; }); } // Setup Lexical Storage Interface this.setupLexicalStorageControls(); } setupLexicalStorageControls() { // Chain Operations const lexicalPutBtn = document.getElementById('lexical-put-btn'); const lexicalGetBtn = document.getElementById('lexical-get-btn'); const lexicalValBtn = document.getElementById('lexical-val-btn'); const lexicalUpdateBtn = document.getElementById('lexical-update-btn'); const lexicalDeleteBtn = document.getElementById('lexical-delete-btn'); // Set Operations const lexicalSetBtn = document.getElementById('lexical-set-btn'); const lexicalMapBtn = document.getElementById('lexical-map-btn'); // Property Access const lexicalProxySetBtn = document.getElementById('lexical-proxy-set-btn'); const lexicalProxyGetBtn = document.getElementById('lexical-proxy-get-btn'); // Utility Operations const lexicalExistsBtn = document.getElementById('lexical-exists-btn'); const lexicalKeysBtn = document.getElementById('lexical-keys-btn'); const lexicalPathBtn = document.getElementById('lexical-path-btn'); // Chain Operations Event Handlers if (lexicalPutBtn) { lexicalPutBtn.addEventListener('click', async () => { const path = document.getElementById('lexical-path')?.value?.trim(); const data = document.getElementById('lexical-data')?.value?.trim(); if (!path || !data) { this.addLexicalLogEntry('❌ Error: Both path and data are required'); return; } try { const parsedData = JSON.parse(data); const lex = this.mesh.distributedStorage.lexical(); const pathParts = path.split('.'); let current = lex; for (const part of pathParts) { current = current.get(part); } await current.put(parsedData); this.addLexicalLogEntry(`βœ… Put data at path: ${path}`); } catch (error) { this.addLexicalLogEntry(`❌ Error: ${error.message}`); } }); } if (lexicalGetBtn) { lexicalGetBtn.addEventListener('click', async () => { const path = document.getElementById('lexical-path')?.value?.trim(); const property = document.getElementById('lexical-property')?.value?.trim(); if (!path || !property) { this.addLexicalLogEntry('❌ Error: Both path and property are required'); return; } try { const lex = this.mesh.distributedStorage.lexical(); const pathParts = path.split('.'); let current = lex; for (const part of pathParts) { current = current.get(part); } const value = await current.get(property).val(); this.addLexicalLogEntry(`πŸ“„ ${path}.${property}: ${JSON.stringify(value)}`); } catch (error) { this.addLexicalLogEntry(`❌ Error: ${error.message}`); } }); } if (lexicalValBtn) { lexicalValBtn.addEventListener('click', async () => { const path = document.getElementById('lexical-path')?.value?.trim(); if (!path) { this.addLexicalLogEntry('❌ Error: Path is required'); return; } try { const lex = this.mesh.distributedStorage.lexical(); const pathParts = path.split('.'); let current = lex; for (const part of pathParts) { current = current.get(part); } const value = await current.val(); this.addLexicalLogEntry(`πŸ“„ Full object at ${path}: ${JSON.stringify(value, null, 2)}`); } catch (error) { this.addLexicalLogEntry(`❌ Error: ${error.message}`); } }); } if (lexicalUpdateBtn) { lexicalUpdateBtn.addEventListener('click', async () => { const path = document.getElementById('lexical-path')?.value?.trim(); const data = document.getElementById('lexical-data')?.value?.trim(); if (!path || !data) { this.addLexicalLogEntry('❌ Error: Both path and data are required'); return; } try { const parsedData = JSON.parse(data); const lex = this.mesh.distributedStorage.lexical(); const pathParts = path.split('.'); let current = lex; for (const part of pathParts) { current = current.get(part); } await current.update(parsedData); this.addLexicalLogEntry(`βœ… Updated data at path: ${path}`); } catch (error) { this.addLexicalLogEntry(`❌ Error: ${error.message}`); } }); } if (lexicalDeleteBtn) { lexicalDeleteBtn.addEventListener('click', async () => { const path = document.getElementById('lexical-path')?.value?.trim(); if (!path) { this.addLexicalLogEntry('❌ Error: Path is required'); return; } try { const lex = this.mesh.distributedStorage.lexical(); const pathParts = path.split('.'); let current = lex; for (const part of pathParts) { current = current.get(part); } await current.delete(); this.addLexicalLogEntry(`βœ… Deleted data at path: ${path}`); } catch (error) { this.addLexicalLogEntry(`❌ Error: ${error.message}`); } }); } // Set Operations Event Handlers if (lexicalSetBtn) { lexicalSetBtn.addEventListener('click', async () => { const path = document.getElementById('lexical-set-path')?.value?.trim(); const data = document.getElementById('lexical-set-data')?.value?.trim(); if (!path || !data) { this.addLexicalLogEntry('❌ Error: Both path and data are required'); return; } try { const parsedData = JSON.parse(data); const lex = this.mesh.distributedStorage.lexical(); const pathParts = path.split('.'); let current = lex; for (const part of pathParts) { current = current.get(part); } await current.set(parsedData); this.addLexicalLogEntry(`βœ… Set data at path: ${path}`); } catch (error) { this.addLexicalLogEntry(`❌ Error: ${error.message}`); } }); } if (lexicalMapBtn) { lexicalMapBtn.addEventListener('click', async () => { const path = document.getElementById('lexical-set-path')?.value?.trim(); if (!path) { this.addLexicalLogEntry('❌ Error: Path is required'); return; } try { const lex = this.mesh.distributedStorage.lexical(); const pathParts = path.split('.'); let current = lex; for (const part of pathParts) { current = current.get(part); } const mapResults = []; const unsubscribe = current.map().on((value, key) => { mapResults.push({ key, value }); this.addLexicalLogEntry(`πŸ—ΊοΈ Map result - ${key}: ${JSON.stringify(value)}`); }); // Stop mapping after a short delay setTimeout(() => { if (unsubscribe) unsubscribe(); this.addLexicalLogEntry(`βœ… Map operation completed with ${mapResults.length} results`); }, 1000); } catch (error) { this.addLexicalLogEntry(`❌ Error: ${error.message}`); } }); } // Property Access Event Handlers if (lexicalProxySetBtn) { lexicalProxySetBtn.addEventListener('click', async () => { const path = document.getElementById('lexical-proxy-path')?.value?.trim(); const value = document.getElementById('lexical-proxy-value')?.value?.trim(); if (!path || !value) { this.addLexicalLogEntry('❌ Error: Both path and value are required'); return; } try { const lex = this.mesh.distributedStorage.lexical(); const pathParts = path.split('.'); // Use proxy property access let current = lex; for (const part of pathParts.slice(0, -1)) { current = current[part]; } const lastPart = pathParts[pathParts.length - 1]; await current[lastPart].put(value); this.addLexicalLogEntry(`βœ… Set via proxy: ${path} = ${value}`); } catch (error) { this.addLexicalLogEntry(`❌ Error: ${error.message}`); } }); } if (lexicalProxyGetBtn) { lexicalProxyGetBtn.addEventListener('click', async () => { const path = document.getElementById('lexical-proxy-path')?.value?.trim(); if (!path) { this.addLexicalLogEntry('❌ Error: Path is required'); return; } try { const lex = this.mesh.distributedStorage.lexical(); const pathParts = path.split('.'); // Use proxy property access let current = lex; for (const part of pathParts.slice(0, -1)) { current = current[part]; } const lastPart = pathParts[pathParts.length - 1]; const value = await current[lastPart].val(); this.addLexicalLogEntry(`πŸ“„ Proxy get ${path}: ${JSON.stringify(value)}`); } catch (error) { this.addLexicalLogEntry(`❌ Error: ${error.message}`); } }); } // Utility Operations Event Handlers if (lexicalExistsBtn) { lexicalExistsBtn.addEventListener('click', async () => { const path = document.getElementById('lexical-util-path')?.value?.trim(); if (!path) { this.addLexicalLogEntry('❌ Error: Path is required'); return; } try { const lex = this.mesh.distributedStorage.lexical(); const pathParts = path.split('.'); let current = lex; for (const part of pathParts) { current = current.get(part); } const exists = await current.exists(); this.addLexicalLogEntry(`πŸ” ${path} exists: ${exists}`); } catch (error) { this.addLexicalLogEntry(`❌ Error: ${error.message}`); } }); } if (lexicalKeysBtn) { lexicalKeysBtn.addEventListener('click', async () => { const path = document.getElementById('lexical-util-path')?.value?.trim(); if (!path) { this.addLexicalLogEntry('❌ Error: Path is required'); return; } try { const lex = this.mesh.distributedStorage.lexical(); const pathParts = path.split('.'); let current = lex; for (const part of pathParts) { current = current.get(part); } const keys = await current.keys(); this.addLexicalLogEntry(`πŸ”‘ Keys at ${path}: [${keys.join(', ')}]`); } catch (error) { this.addLexicalLogEntry(`❌ Error: ${error.message}`); } }); } if (lexicalPathBtn) { lexicalPathBtn.addEventListener('click', async () => { const path = document.getElementById('lexical-util-path')?.value?.trim(); if (!path) { this.addLexicalLogEntry('❌ Error: Path is required'); return; } try { const lex = this.mesh.distributedStorage.lexical(); const pathParts = path.split('.'); let current = lex; for (const part of pathParts) { current = current.get(part); } const fullPath = current.getPath(); this.addLexicalLogEntry(`πŸ›€οΈ Full storage path: ${fullPath}`); } catch (error) { this.addLexicalLogEntry(`❌ Error: ${error.message}`); } }); } } async handleStorageStore(isUpdate = false) { const key = document.getElementById('storage-key')?.value?.trim(); const value = document.getElementById('storage-value')?.value?.trim(); const encrypt = document.getElementById('storage-encrypt')?.checked ?? true; const isPublic = document.getElementById('storage-public')?.checked ?? false; const immutable = document.getElementById('storage-immutable')?.checked ?? false; const enableCrdt = document.getElementById('storage-crdt')?.checked ?? false; const ttl = parseInt(document.getElementById('storage-ttl')?.value) || 0; if (!key || !value) { this.addStorageLogEntry('❌ Error: Both key and value are required'); return; } try { if (!this.mesh.distributedStorage) { this.addStorageLogEntry('❌ Error: Distributed storage not available'); return; } let parsedValue; try { parsedValue = JSON.parse(value); } catch { parsedValue = value; // Use as string if not valid JSON } const options = { encrypt, isPublic, isImmutable: immutable, enableCRDT: enableCrdt, ttl: ttl > 0 ? ttl * 1000 : undefined // Convert to milliseconds }; let success; if (isUpdate) { success = await this.mesh.distributedStorage.update(key, parsedValue, options); if (success) { this.addStorageLogEntry(`πŸ”„ Updated: ${key} = ${JSON.stringify(parsedValue)}`); } else { this.addStorageLogEntry(`❌ Failed to update: ${key}`); } } else { success = await this.mesh.distributedStorage.store(key, parsedValue, options); if (success) { this.addStorageLogEntry(`πŸ’Ύ Stored: ${key} = ${JSON.stringify(parsedValue)}`); } else { this.addStorageLogEntry(`❌ Failed to store: ${key}`); } } if (success) { const optionsStr = []; if (encrypt) optionsStr.push('encrypted'); if (isPublic) optionsStr.push('public'); if (immutable) optionsStr.push('immutable'); if (enableCrdt) optionsStr.push('CRDT'); if (ttl > 0) optionsStr.push(`TTL: ${ttl}s`); if (optionsStr.length > 0) { this.addStorageLogEntry(` Options: ${optionsStr.join(', ')}`); } this.updateStorageStatus(); } } catch (error) { this.addStorageLogEntry(`❌ Error ${isUpdate ? 'updating' : 'storing'} ${key}: ${error.message}`); } } addStorageLogEntry(message) { const logElement = document.getElementById('storage-log'); if (!logElement) return; const entry = document.createElement('div'); entry.className = 'log-entry'; entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; logElement.appendChild(entry); logElement.scrollTop = logElement.scrollHeight; // Keep only last 100 entries while (logElement.children.length > 100) { logElement.removeChild(logElement.firstChild); } } addLexicalLogEntry(message) { const logElement = document.getElementById('lexical-log'); if (!logElement) return; const entry = document.createElement('div'); entry.className = 'log-entry'; entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; logElement.appendChild(entry); logElement.scrollTop = logElement.scrollHeight; // Keep only last 100 entries while (logElement.children.length > 100) { logElement.removeChild(logElement.firstChild); } } updateStorageStatus() { if (!this.mesh.distributedStorage) { document.getElementById('storage-status').textContent = 'Not Available'; document.getElementById('storage-item-count').textContent = '0'; document.getElementById('storage-total-size').textContent = '0 bytes'; return; } const isEnabled = this.mesh.distributedStorage.isEnabled(); document.getElementById('storage-status').textContent = isEnabled ? 'Enabled' : 'Disabled'; // Get storage stats if available this.mesh.distributedStorage.getStats().then(stats => { document.getElementById('storage-item-count').textContent = stats.itemCount.toString(); document.getElementById('storage-total-size').textContent = this.formatBytes(stats.totalSize); }).catch(() => { document.getElementById('storage-item-count').textContent = '0'; document.getElementById('storage-total-size').textContent = '0 bytes'; }); } formatBytes(bytes) { if (bytes === 0) return '0 bytes'; const k = 1024; const sizes = ['bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } setupCryptoControls() { // Key Management Controls const generateBtn = document.getElementById('crypto-generate-btn'); const resetBtn = document.getElementById('crypto-reset-btn'); const selfTestBtn = document.getElementById('crypto-self-test-btn'); // User Authentication Controls const loginBtn = document.getElementById('crypto-login-btn'); const aliasInput = document.getElementById('crypto-alias'); const passwordInput = document.getElementById('crypto-password'); // Messaging Controls const testMessageInput = document.getElementById('crypto-test-message'); const testPeerInput = document.getElementById('crypto-test-peer'); const sendEncryptedBtn = document.getElementById('crypto-send-encrypted-btn'); // Group Encryption Controls const groupIdInput = document.getElementById('crypto-group-id'); const createGroupBtn = document.getElementById('crypto-create-group-btn'); const groupMessageInput = document.getElementById('crypto-group-message');