'use strict'; // ── WebSocket to server ────────────────────────────────────────────────────── let ws; let connected = false; let sessionActive = false; let videoActive = false; function connectWS() { const proto = location.protocol === 'https:' ? 'wss' : 'ws'; ws = new WebSocket(`${proto}://${location.host}/ws`); ws.onopen = () => { connected = true; }; ws.onclose = () => { connected = false; setStatus(false, 'Disconnected — reconnecting…'); setTimeout(connectWS, 2000); }; ws.onerror = () => {}; ws.onmessage = (e) => { let msg; try { msg = JSON.parse(e.data); } catch { return; } handleServerMessage(msg); }; } function handleServerMessage(msg) { if (msg.type === 'status') { sessionActive = !!msg.sessionID; setStatus(msg.connected && sessionActive, msg.connected ? (sessionActive ? 'Connected • ' + msg.sessionID.slice(0, 8) + '…' : 'Connecting…') : 'Disconnected'); return; } if (msg.type === 'jiboEvent') { handleJiboEvent(msg.body, msg.txId); } } function handleJiboEvent(body, txId) { if (!body) return; const evt = body.Event || body.ResponseString || '?'; logEvent(evt, body, txId); } // ── REST helpers ───────────────────────────────────────────────────────────── async function api(method, path, body) { try { const opts = { method, headers: { 'Content-Type': 'application/json' } }; if (body) opts.body = JSON.stringify(body); const res = await fetch(path, opts); return await res.json(); } catch (err) { logEvent('api-error: ' + path, { error: err.message }, null); } } const post = (path, body) => api('POST', path, body); const get = (path) => api('GET', path); // ── Status ─────────────────────────────────────────────────────────────────── function setStatus(ok, label) { const dot = document.getElementById('status-dot'); const lbl = document.getElementById('status-label'); dot.className = ok ? 'ok' : ''; lbl.textContent = label; } // ── Animation Control ──────────────────────────────────────────────────────── document.getElementById('btn-play-anim').addEventListener('click', () => { const name = document.getElementById('anim-select').value; if (name) { post('/api/display/anim', { name }); logEvent('Animation Played', { animation: name }, null); } }); document.getElementById('btn-play-sequence').addEventListener('click', () => { const sequence = document.getElementById('sequence-list').value .split('\n') .map(line => line.trim()) .filter(line => line.length > 0); if (sequence.length === 0) { alert('Please enter at least one animation'); return; } const repeatCount = parseInt(document.getElementById('repeat-count').value) || 1; const delay = parseInt(document.getElementById('anim-delay').value) || 500; post('/api/display/anim-sequence', { sequence, repeatCount, delay }); logEvent('Sequence Started', { count: sequence.length, repeats: repeatCount }, null); }); document.getElementById('btn-stop-sequence').addEventListener('click', () => { post('/api/cancel', {}); logEvent('Sequence Stopped', {}, null); }); // ── Camera / Video ──────────────────────────────────────────────────────────── let videoTxId = null; document.getElementById('btn-video-start').addEventListener('click', async () => { const r = await post('/api/video/start', { duration: 0 }); if (r) videoTxId = r.txId; document.getElementById('video-status').textContent = 'Waiting for VideoReady…'; }); document.getElementById('btn-video-stop').addEventListener('click', () => { post('/api/video/stop', {}); stopVideoFeed(); }); function startVideoFeed(uri) { const feed = document.getElementById('camera-feed'); const noFeed = document.getElementById('camera-no-feed'); feed.src = '/proxy/stream?uri=' + encodeURIComponent(uri); feed.style.display = 'block'; noFeed.style.display = 'none'; videoActive = true; } function stopVideoFeed() { const feed = document.getElementById('camera-feed'); feed.src = ''; feed.style.display = 'none'; document.getElementById('camera-no-feed').style.display = ''; videoActive = false; } // ── Take Photo ──────────────────────────────────────────────────────────────── document.getElementById('btn-photo').addEventListener('click', () => { post('/api/photo', { camera: 'right', resolution: document.getElementById('photo-res').value }); }); function addPhoto(url) { const strip = document.getElementById('photo-strip'); const img = document.createElement('img'); img.src = url; img.title = url; img.addEventListener('click', () => openPhotoModal(img.src)); strip.prepend(img); } // ── Photo modal ─────────────────────────────────────────────────────────────── function openPhotoModal(src) { document.getElementById('photo-modal-img').src = src; document.getElementById('photo-modal').classList.add('open'); } document.getElementById('photo-modal').addEventListener('click', (e) => { if (e.target === e.currentTarget || e.target.id === 'photo-modal-close') { document.getElementById('photo-modal').classList.remove('open'); } }); // ── Tabs ────────────────────────────────────────────────────────────────────── document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', function () { document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active')); this.classList.add('active'); document.getElementById(this.dataset.tab).classList.add('active'); }); }); // ── Event log ───────────────────────────────────────────────────────────────── const MAX_LOG = 200; function logEvent(eventName, body, txId) { const log = document.getElementById('event-log'); const now = new Date().toLocaleTimeString(); let cls = 'evt'; if (eventName.toLowerCase().includes('error')) cls = 'evt-error'; let detail = ''; if (body.animation) detail = ' ' + body.animation; const el = document.createElement('div'); el.className = 'log-entry'; el.innerHTML = `${now} ${eventName}${detail}`; log.prepend(el); while (log.children.length > MAX_LOG) log.removeChild(log.lastChild); } document.getElementById('btn-clear-log').addEventListener('click', () => { document.getElementById('event-log').innerHTML = ''; }); // ── Back to home ────────────────────────────────────────────────────────────── document.getElementById('btn-back-home')?.addEventListener('click', () => { window.location.href = 'index.html'; }); // ── Initialization ──────────────────────────────────────────────────────────── connectWS();