(async () => { const logBox = document.getElementById('logBox'); const userPubkeyEl = document.getElementById('userPubkey'); const valSteps = document.getElementById('valSteps'); const valUnlocks = document.getElementById('valUnlocks'); const valBattery = document.getElementById('valBattery'); const valData = document.getElementById('valData'); const subBattery = document.getElementById('subBattery'); const mediaTitle = document.getElementById('mediaTitle'); const mediaArtist = document.getElementById('mediaArtist'); const mediaPos = document.getElementById('mediaPos'); const mediaDur = document.getElementById('mediaDur'); const mediaProgress = document.getElementById('mediaProgress'); const mediaHistory = document.getElementById('mediaHistory'); const nowPlayingBadge = document.getElementById('nowPlayingBadge'); const btnRefresh = document.getElementById('btnRefresh'); let currentPubkey = null; function appendLog(msg) { console.log('[LOG]', msg); const time = new Date().toLocaleTimeString(); const div = document.createElement('div'); div.className = 'log-item'; div.textContent = `[${time}] ${msg}`; logBox.prepend(div); } function formatBytes(bytes) { if (!bytes || bytes === 0) return '0 MB'; const mb = bytes / (1024 * 1024); return mb >= 1000 ? `${(mb / 1024).toFixed(2)} GB` : `${mb.toFixed(1)} MB`; } function formatSeconds(sec) { if (!sec || isNaN(sec)) return '0:00'; const m = Math.floor(sec / 60); const s = Math.floor(sec % 60); return `${m}:${s < 10 ? '0' : ''}${s}`; } async function initUser() { if (!window.nostr) { appendLog('Warning: window.nostr bridge not detected'); userPubkeyEl.textContent = 'Standalone Mode'; return; } try { currentPubkey = await window.nostr.getPublicKey(); userPubkeyEl.textContent = `${currentPubkey.slice(0, 8)}...${currentPubkey.slice(-6)}`; appendLog(`Authenticated user: ${currentPubkey}`); } catch (e) { appendLog(`User fetch error: ${e.message}`); userPubkeyEl.textContent = 'Not Logged In'; } } async function loadTelemetry() { if (!currentPubkey) { appendLog('Not logged in. Showing empty telemetry state.'); clearDisplay(); return; } appendLog('Syncing SNIP-10 telemetry data...'); if (!window.nostrdb) { appendLog('window.nostrdb not present; loading fallback demo data'); updateFallbackData(); return; } try { // 1. Steps (Kind 1359) const stepEvents = await window.nostrdb.query([{ kinds: [1359], authors: [currentPubkey], limit: 30 }]); let todaySteps = 0; const todayStr = new Date().toISOString().split('T')[0]; if (stepEvents && stepEvents.length > 0) { const latestStep = stepEvents[0]; const latestDate = latestStep.tags.find(t => t[0] === 'timestamp')?.[1]; if (latestDate === todayStr) { todaySteps = Number(latestStep.content) || 0; } valSteps.textContent = todaySteps.toLocaleString(); appendLog(`Fetched HolyFit steps: ${todaySteps}`); } else { valSteps.textContent = '0'; } // 2. Unlocks (Kind 30342) const unlockEvents = await window.nostrdb.query([{ kinds: [30342], authors: [currentPubkey], limit: 30 }]); let todayUnlocks = 0; if (unlockEvents && unlockEvents.length > 0) { const latestUnlock = unlockEvents[0]; const latestDate = latestUnlock.tags.find(t => t[0] === 'd')?.[1]; if (latestDate === todayStr) { todayUnlocks = Number(latestUnlock.content) || 0; } valUnlocks.textContent = todayUnlocks; appendLog(`Fetched NUnlock count: ${todayUnlocks}`); } else { valUnlocks.textContent = '0'; } // 3. Battery (Kind 30343) const batteryEvents = await window.nostrdb.query([{ kinds: [30343], authors: [currentPubkey], limit: 5 }]); if (batteryEvents && batteryEvents.length > 0) { try { const bData = JSON.parse(batteryEvents[0].content); valBattery.textContent = `${bData.level || 0}%`; subBattery.textContent = `${bData.is_charging ? '⚡ Charging' : 'Discharging'} (${bData.temp_c || '--'}°C)`; appendLog(`Battery: ${bData.level}% (${bData.temp_c}°C)`); } catch (e) { valBattery.textContent = '--%'; } } else { valBattery.textContent = '--%'; subBattery.textContent = 'No battery status'; } // 4. Data Usage (Kind 34557) const dataEvents = await window.nostrdb.query([{ kinds: [34557], authors: [currentPubkey], limit: 5 }]); if (dataEvents && dataEvents.length > 0) { try { const dData = JSON.parse(dataEvents[0].content); const total = (dData.mobile_bytes_rx || 0) + (dData.mobile_bytes_tx || 0) + (dData.wifi_bytes_rx || 0) + (dData.wifi_bytes_tx || 0); valData.textContent = formatBytes(total); appendLog(`Data usage total: ${formatBytes(total)}`); } catch (e) { valData.textContent = '-- MB'; } } else { valData.textContent = '0 MB'; } // 5. SaintStream Recent Media (Kind 30344) const mediaEvents = await window.nostrdb.query([{ kinds: [30344], authors: [currentPubkey], limit: 10 }]); mediaHistory.innerHTML = ''; if (mediaEvents && mediaEvents.length > 0) { mediaEvents.forEach(evt => { const title = evt.tags.find(t => t[0] === 'title')?.[1] || 'Unknown Track'; const artist = evt.tags.find(t => t[0] === 'artist')?.[1] || 'Unknown Artist'; const li = document.createElement('li'); li.innerHTML = `${title} — ${artist}${new Date(evt.created_at * 1000).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}`; mediaHistory.appendChild(li); }); appendLog(`Loaded ${mediaEvents.length} media history events`); } else { const li = document.createElement('li'); li.className = 'empty-item'; li.textContent = 'No recent tracks found'; mediaHistory.appendChild(li); } // 6. Now Playing (Kind 36787) const nowPlaying = await window.nostrdb.query([{ kinds: [36787], authors: [currentPubkey], limit: 1 }]); if (nowPlaying && nowPlaying.length > 0) { const np = nowPlaying[0]; const title = np.tags.find(t => t[0] === 'title')?.[1] || np.content; const artist = np.tags.find(t => t[0] === 'artist')?.[1] || 'Unknown'; const pos = Number(np.tags.find(t => t[0] === 'position')?.[1] || 0); const dur = Number(np.tags.find(t => t[0] === 'duration')?.[1] || 1); mediaTitle.textContent = title; mediaArtist.textContent = artist; mediaPos.textContent = formatSeconds(pos); mediaDur.textContent = formatSeconds(dur); mediaProgress.style.width = `${Math.min(100, (pos / dur) * 100)}%`; nowPlayingBadge.textContent = 'Playing'; nowPlayingBadge.style.background = 'rgba(16, 185, 129, 0.2)'; nowPlayingBadge.style.color = '#a7f3d0'; } else { mediaTitle.textContent = 'No Track Playing'; mediaArtist.textContent = '--'; mediaPos.textContent = '0:00'; mediaDur.textContent = '0:00'; mediaProgress.style.width = '0%'; nowPlayingBadge.textContent = 'Idle'; nowPlayingBadge.style.background = ''; nowPlayingBadge.style.color = ''; } // Process 7-day chart data const chartDays = []; const stepsData = []; const unlocksData = []; const todayObj = new Date(); for (let i = 6; i >= 0; i--) { const d = new Date(todayObj); d.setDate(todayObj.getDate() - i); const dateStr = d.toISOString().split('T')[0]; const dayName = d.toLocaleDateString([], { weekday: 'short' }); chartDays.push({ dateStr, dayName }); stepsData.push(0); unlocksData.push(0); } // Match step events to chart days stepEvents.forEach(evt => { const dateStr = evt.tags.find(t => t[0] === 'timestamp')?.[1]; const dayIdx = chartDays.findIndex(c => c.dateStr === dateStr); if (dayIdx !== -1) { stepsData[dayIdx] = Number(evt.content) || 0; } }); // Match unlock events to chart days unlockEvents.forEach(evt => { const dateStr = evt.tags.find(t => t[0] === 'd')?.[1]; const dayIdx = chartDays.findIndex(c => c.dateStr === dateStr); if (dayIdx !== -1) { unlocksData[dayIdx] = Number(evt.content) || 0; } }); drawChart(chartDays.map(c => c.dayName), stepsData, unlocksData); } catch (e) { appendLog(`Query error: ${e.message}`); } } function clearDisplay() { valSteps.textContent = '--'; valUnlocks.textContent = '--'; valBattery.textContent = '--%'; subBattery.textContent = 'Please log in'; valData.textContent = '-- MB'; mediaTitle.textContent = 'No Track Playing'; mediaArtist.textContent = '--'; mediaPos.textContent = '0:00'; mediaDur.textContent = '0:00'; mediaProgress.style.width = '0%'; nowPlayingBadge.textContent = 'Logged out'; mediaHistory.innerHTML = '
  • No recent tracks found
  • '; // Draw empty chart const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; drawChart(days, [0,0,0,0,0,0,0], [0,0,0,0,0,0,0]); } function updateFallbackData() { valSteps.textContent = '8,439'; valUnlocks.textContent = '42'; valBattery.textContent = '85%'; subBattery.textContent = '⚡ Charging (24.8°C)'; valData.textContent = '74.2 MB'; mediaTitle.textContent = 'Sandstorm'; mediaArtist.textContent = 'Darude'; mediaPos.textContent = '0:45'; mediaDur.textContent = '3:52'; mediaProgress.style.width = '19%'; nowPlayingBadge.textContent = 'Playing'; mediaHistory.innerHTML = `
  • Sandstorm — Darude3:45 PM
  • Resonance — HOME3:10 PM
  • Verdana Vibe — Nostr Session2:30 PM
  • `; const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; const stepsData = [6200, 7800, 9100, 5400, 8439, 10200, 4500]; const unlocksData = [55, 48, 32, 60, 42, 25, 68]; drawChart(days, stepsData, unlocksData); } function drawChart(days, stepsData, unlocksData) { const canvas = document.getElementById('balanceCanvas'); if (!canvas || !canvas.getContext) return; const ctx = canvas.getContext('2d'); const width = canvas.width; const height = canvas.height; ctx.clearRect(0, 0, width, height); // Grid lines ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)'; ctx.lineWidth = 1; for (let y = 30; y < height; y += 30) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke(); } const maxSteps = Math.max(...stepsData, 1000); const maxUnlocks = Math.max(...unlocksData, 10); // Draw Steps line (Green) ctx.strokeStyle = '#10b981'; ctx.lineWidth = 2.5; ctx.beginPath(); stepsData.forEach((val, i) => { const x = 30 + i * ((width - 60) / (days.length - 1)); const y = height - 25 - (val / maxSteps) * (height - 50); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); // Draw Unlocks line (Purple) ctx.strokeStyle = '#8b5cf6'; ctx.lineWidth = 2; ctx.setLineDash([4, 4]); ctx.beginPath(); unlocksData.forEach((val, i) => { const x = 30 + i * ((width - 60) / (days.length - 1)); const y = height - 25 - (val / maxUnlocks) * (height - 50); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); ctx.setLineDash([]); // Draw Day Labels ctx.fillStyle = '#9ca3af'; ctx.font = '10px Outfit, sans-serif'; days.forEach((day, i) => { const x = 30 + i * ((width - 60) / (days.length - 1)); ctx.fillText(day, x - 8, height - 8); }); // Legend ctx.fillStyle = '#10b981'; ctx.fillText('— Steps', 10, 16); ctx.fillStyle = '#8b5cf6'; ctx.fillText('- - Unlocks', 70, 16); } const activeSockets = []; async function syncRelays() { if (!currentPubkey) return; appendLog('Starting SNIP-10 wellbeing relay discovery...'); // 1. Determine bootstrap relays let bootstrapRelays = ['wss://relay.nostrapps.com']; if (window.napp && window.napp.utils && window.napp.utils.loadRelayList) { try { const list = await window.napp.utils.loadRelayList(currentPubkey); if (list && list.length > 0) { const fetched = list.map(item => typeof item === 'string' ? item : item.url || item.URL).filter(Boolean); if (fetched.length > 0) { bootstrapRelays = fetched; } } } catch (e) { appendLog(`Bootstrap relay list fetch error: ${e.message}`); } } // Close any previous active sockets closeActiveSockets(); appendLog(`Connecting to bootstrap relays: ${bootstrapRelays.join(', ')}`); let got10323 = false; let fallbackTimeout = setTimeout(() => { if (!got10323) { appendLog('No Kind 10323 Wellbeing Relay List found. Falling back to bootstrap relays.'); const fallbackConfigs = bootstrapRelays.map(url => ({ url, kinds: [1359, 30342, 30343, 34557, 30344, 36787] })); subscribeToTelemetry(fallbackConfigs); } }, 3000); bootstrapRelays.forEach(url => { try { let targetUrl = url; if (!targetUrl.includes('://')) { targetUrl = 'wss://' + targetUrl; } const ws = new WebSocket(targetUrl); activeSockets.push(ws); ws.onopen = () => { appendLog(`Connected to bootstrap relay: ${targetUrl}`); const subId = 'wb_relays_' + Math.random().toString(36).substring(2, 9); ws.send(JSON.stringify([ "REQ", subId, { kinds: [10323], authors: [currentPubkey], limit: 1 } ])); }; ws.onmessage = async (e) => { try { const msg = JSON.parse(e.data); if (msg[0] === "EVENT" && msg[2]) { const event = msg[2]; if (event.kind === 10323) { got10323 = true; clearTimeout(fallbackTimeout); // Parse 10323 relays into [{ url, kinds }] const relayConfigs = []; event.tags.forEach(t => { if (t[0] === 'relay' && t[1]) { const rUrl = t[1]; const target = t[2] ? t[2].toLowerCase() : ''; let kinds = [1359, 30342, 30343, 34557, 30344, 36787]; if (target === 'holyfit') kinds = [1359]; else if (target === 'nunlock') kinds = [30342]; else if (target === 'sistercharge') kinds = [30343]; else if (target === 'cellibacy') kinds = [34557]; else if (target === 'saintstream') kinds = [30344, 36787]; relayConfigs.push({ url: rUrl, kinds }); } }); if (relayConfigs.length > 0) { appendLog(`Discovered ${relayConfigs.length} specialized Wellbeing Relays from Kind 10323.`); // Save event to local DB if (window.nostrdb && window.nostrdb.add) { try { await window.nostrdb.add(event); } catch (dbErr) { // Ignore duplicate event errors } } // Switch to wellbeing relays subscribeToTelemetry(relayConfigs); } else { appendLog('Kind 10323 found but contained no relay tags. Using bootstrap relays.'); const fallbackConfigs = bootstrapRelays.map(u => ({ url: u, kinds: [1359, 30342, 30343, 34557, 30344, 36787] })); subscribeToTelemetry(fallbackConfigs); } } } } catch (err) { // Ignore } }; ws.onerror = () => {}; ws.onclose = () => {}; } catch (err) { appendLog(`Bootstrap connection failed for ${url}: ${err.message}`); } }); } function closeActiveSockets() { while (activeSockets.length > 0) { const ws = activeSockets.pop(); try { ws.close(); } catch(e) {} } } function subscribeToTelemetry(relayConfigs) { // Close the bootstrap sockets before opening new ones closeActiveSockets(); appendLog(`Connecting to ${relayConfigs.length} telemetry relays...`); relayConfigs.forEach(cfg => { const url = cfg.url; const kinds = cfg.kinds; try { let targetUrl = url; if (!targetUrl.includes('://')) { targetUrl = 'wss://' + targetUrl; } const ws = new WebSocket(targetUrl); activeSockets.push(ws); ws.onopen = () => { appendLog(`Connected to telemetry relay: ${targetUrl} (Subscribing to kinds: ${kinds.join(', ')})`); const subId = 'wb_metrics_' + Math.random().toString(36).substring(2, 9); ws.send(JSON.stringify([ "REQ", subId, { kinds: kinds, authors: [currentPubkey] } ])); }; ws.onmessage = async (e) => { try { const msg = JSON.parse(e.data); if (msg[0] === "EVENT" && msg[2]) { const event = msg[2]; appendLog(`Received event kind ${event.kind} from ${targetUrl}`); if (window.nostrdb && window.nostrdb.add) { try { await window.nostrdb.add(event); } catch (dbErr) { // Ignore duplicate event errors } } loadTelemetry(); } } catch (err) { // Ignore parse errors } }; ws.onerror = () => { appendLog(`Telemetry relay error: ${targetUrl}`); }; ws.onclose = () => { appendLog(`Telemetry relay closed: ${targetUrl}`); }; } catch (err) { appendLog(`Failed to connect to telemetry relay ${url}: ${err.message}`); } }); } btnRefresh.addEventListener('click', () => { loadTelemetry(); syncRelays(); }); await initUser(); await loadTelemetry(); if (currentPubkey) { syncRelays(); } })();