(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 = '