peerpigeon
Version:
WebRTC-based peer-to-peer mesh networking library with intelligent routing and signaling server
1,439 lines (1,207 loc) β’ 124 kB
JavaScript
// 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');