let socket = null; let sessionId = null; let workerId = null; let token = null; let heartbeatInterval = null; let deviceKeys = null; function log(msg) { const el = document.getElementById('log'); const line = document.createElement('div'); line.textContent = new Date().toLocaleTimeString() + ' ' + msg; el.appendChild(line); el.scrollTop = el.scrollHeight; console.log(msg); } function parseSessionFromUrl() { const params = new URLSearchParams(window.location.search); sessionId = params.get('session_id'); token = params.get('token'); return { sessionId, token }; } async function connectToSpace() { parseSessionFromUrl(); if (!sessionId || !token) { log('No session_id or token in URL'); return; } workerId = 'wk_' + Math.random().toString(36).slice(2, 10); log('Connecting as ' + workerId + ' to session ' + sessionId); const wsUrl = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws/worker/' + sessionId + '/' + workerId; socket = new WebSocket(wsUrl); socket.onopen = async () => { log('WebSocket connected'); updateStatus('connecting'); deviceKeys = await ReceiptCrypto.loadDeviceKeypair(); const runtime = PhoneRuntime.selectBestRuntime(); sendWorkerHello(runtime); }; socket.onmessage = (event) => { const msg = JSON.parse(event.data); handleSocketMessage(msg); }; socket.onclose = () => { log('WebSocket closed'); updateStatus('offline'); stopHeartbeat(); }; socket.onerror = (err) => { log('WebSocket error'); updateStatus('offline'); }; document.getElementById('btnConnect').disabled = true; document.getElementById('btnDisconnect').disabled = false; } function sendWorkerHello(runtimeType) { if (!socket) return; socket.send(JSON.stringify({ op: 'worker_hello', worker_id: workerId, runtime_type: runtimeType, device_public_key: deviceKeys ? deviceKeys.publicKey : null, })); } function detectDeviceInfo() { const battery = navigator.getBattery ? navigator.getBattery() : Promise.resolve({}); battery.then(b => { document.getElementById('batteryValue').textContent = (b.level ? Math.round(b.level * 100) + '%' : 'unknown'); }).catch(() => {}); document.getElementById('networkValue').textContent = navigator.connection ? navigator.connection.effectiveType : 'unknown'; document.getElementById('thermalValue').textContent = 'nominal'; // iOS Web API limitation document.getElementById('runtimeValue').textContent = PhoneRuntime.selectBestRuntime(); } function advertiseCapabilities() { if (!socket) return; const caps = [ { capability_name: 'iphone.text.embedding.private', runtime_type: PhoneRuntime.selectBestRuntime(), model_id: 'mock-embedding' }, { capability_name: 'iphone.image.classify.local', runtime_type: PhoneRuntime.selectBestRuntime(), model_id: 'mock-image' }, { capability_name: 'iphone.privacy.redact.local', runtime_type: PhoneRuntime.selectBestRuntime(), model_id: 'mock-redact' }, ]; socket.send(JSON.stringify({ op: 'capabilities', worker_id: workerId, capabilities: caps })); renderCapabilities(caps); log('Capabilities advertised'); } function handleSocketMessage(msg) { switch (msg.op) { case 'worker_welcome': updateStatus('online'); detectDeviceInfo(); advertiseCapabilities(); startHeartbeat(); break; case 'job_offer': handleJobOffer(msg); break; case 'session_update': log('Session update: ' + JSON.stringify(msg)); break; case 'error': log('Server error: ' + msg.error); break; } } async function handleJobOffer(job) { log('Job offered: ' + job.job_type + ' (' + job.job_id + ')'); // Auto-accept for v1 socket.send(JSON.stringify({ op: 'job_accept', job_id: job.job_id, worker_id: workerId })); const startTime = Date.now(); let output = {}; try { switch (job.job_type) { case 'text_embedding': output = await PhoneRuntime.runTextEmbedding(job.payload.text || ''); break; case 'image_classification': output = await PhoneRuntime.runImageClassification(job.payload.image || ''); break; case 'privacy_redaction': output = await PhoneRuntime.runPrivacyRedaction(job.payload.text || ''); break; default: throw new Error('Unsupported job type: ' + job.job_type); } } catch (err) { log('Job failed: ' + err.message); socket.send(JSON.stringify({ op: 'job_result', job_id: job.job_id, worker_id: workerId, output: { error: err.message }, latency_ms: Date.now() - startTime })); return; } const latencyMs = Date.now() - startTime; const inputHash = await ReceiptCrypto.makeInputHash(job.payload); const outputHash = await ReceiptCrypto.makeOutputHash(output); const deviceSignature = await ReceiptCrypto.signReceipt({ job_id: job.job_id, worker_id: workerId, input_hash: inputHash, output_hash: outputHash, latency_ms: latencyMs, }); socket.send(JSON.stringify({ op: 'job_result', job_id: job.job_id, worker_id: workerId, output: output, latency_ms: latencyMs, input_hash: inputHash, output_hash: outputHash, device_signature: deviceSignature, })); addReceiptCard(job.job_id, job.job_type, latencyMs, outputHash); log('Job completed: ' + job.job_id + ' in ' + latencyMs + 'ms'); } function startHeartbeat() { heartbeatInterval = setInterval(() => { if (!socket || socket.readyState !== WebSocket.OPEN) return; socket.send(JSON.stringify({ op: 'heartbeat', worker_id: workerId, battery_level: 0.8, // mock for v1 thermal_state: 'nominal', jobs_completed: document.querySelectorAll('.receipt-card').length, })); }, 5000); } function stopHeartbeat() { if (heartbeatInterval) clearInterval(heartbeatInterval); heartbeatInterval = null; } function disconnectWorker() { stopHeartbeat(); if (socket) { socket.send(JSON.stringify({ op: 'disconnect', worker_id: workerId, reason: 'user' })); socket.close(); socket = null; } updateStatus('offline'); document.getElementById('btnConnect').disabled = false; document.getElementById('btnDisconnect').disabled = true; } function updateStatus(status) { const badge = document.getElementById('statusBadge'); badge.className = 'status-badge status-' + status; badge.textContent = status.charAt(0).toUpperCase() + status.slice(1); } function renderCapabilities(caps) { const el = document.getElementById('capabilitiesList'); el.innerHTML = caps.map(c => '
' + c.capability_name + '
' + c.runtime_type + ' / ' + c.model_id + '
' ).join(''); } function addReceiptCard(jobId, jobType, latencyMs, outputHash) { const el = document.getElementById('receiptLog'); const card = document.createElement('div'); card.className = 'receipt-card'; card.innerHTML = '
' + jobType + '
Latency: ' + latencyMs + 'ms
' + outputHash.slice(0, 24) + '...
'; el.prepend(card); } // Auto-connect if URL has session params if (window.location.search.includes('session_id')) { window.addEventListener('load', () => { setTimeout(connectToSpace, 500); }); }