/* === MeshCore Analyzer β€” traces.js === */ 'use strict'; (function () { let currentHash = null; let traceData = []; let packetMeta = null; function init(app) { // Check URL for pre-filled hash const params = new URLSearchParams(location.hash.split('?')[1] || ''); const urlHash = params.get('hash') || ''; app.innerHTML = `
`; document.getElementById('traceBtn').addEventListener('click', doTrace); document.getElementById('traceHashInput').addEventListener('keydown', (e) => { if (e.key === 'Enter') doTrace(); }); if (urlHash) doTrace(); } function destroy() { currentHash = null; traceData = []; packetMeta = null; } async function doTrace() { const input = document.getElementById('traceHashInput'); const hash = input.value.trim(); if (!hash) return; currentHash = hash; const results = document.getElementById('traceResults'); results.innerHTML = '
Tracing…
'; try { const [traceResp, pktResp] = await Promise.all([ api(`/traces/${encodeURIComponent(hash)}`), api(`/packets?hash=${encodeURIComponent(hash)}&limit=50`) ]); traceData = traceResp.traces || []; const packets = pktResp.packets || []; if (traceData.length === 0 && packets.length === 0) { results.innerHTML = '
No observations found for this packet hash.
'; return; } // Extract path from first packet that has it let pathHops = []; for (const p of packets) { try { const hops = JSON.parse(p.path_json || '[]'); if (hops.length > 0) { pathHops = hops; break; } } catch {} } // Get packet type info from first packet packetMeta = packets[0] || null; let decoded = null; if (packetMeta) { try { decoded = JSON.parse(packetMeta.decoded_json); } catch {} } renderResults(results, pathHops, decoded); } catch (e) { results.innerHTML = `
Error: ${e.message}
`; } } function renderResults(container, pathHops, decoded) { const uniqueObservers = [...new Set(traceData.map(t => t.observer))]; const typeName = packetMeta ? payloadTypeName(packetMeta.payload_type) : 'β€”'; const typeClass = packetMeta ? payloadTypeColor(packetMeta.payload_type) : 'unknown'; // Compute timing let t0 = null, tLast = null; if (traceData.length > 0) { const times = traceData.map(t => new Date(t.time).getTime()).filter(t => !isNaN(t)); if (times.length) { t0 = Math.min(...times); tLast = Math.max(...times); } } const spreadMs = (t0 !== null && tLast !== null) ? tLast - t0 : 0; container.innerHTML = `
${uniqueObservers.length}
Observers
${traceData.length}
Observations
${spreadMs > 0 ? (spreadMs / 1000).toFixed(1) + 's' : 'β€”'}
Time Spread
${typeName}
Packet Type
${pathHops.length > 0 ? renderPathViz(pathHops) : ''} ${traceData.length > 0 ? renderTimeline(t0, spreadMs) : ''} ${renderObserverTable()} `; makeColumnsResizable('#traceObsTable', 'meshcore-trace-col-widths'); } function renderPathViz(hops) { const arrows = hops.map(h => `${h}`).join('β†’'); return `

Path Visualization

Origin β†’ ${arrows} β†’ Dest
${hops.length} hop${hops.length !== 1 ? 's' : ''} in relay path
`; } function renderTimeline(t0, spreadMs) { // Build timeline bars const barWidth = spreadMs > 0 ? spreadMs : 1; const rows = traceData.map((t, i) => { const time = new Date(t.time); const offsetMs = t0 !== null ? time.getTime() - t0 : 0; const pct = spreadMs > 0 ? (offsetMs / barWidth) * 100 : 50; const snrClass = t.snr != null ? (t.snr >= 0 ? 'good' : t.snr >= -10 ? 'ok' : 'bad') : ''; const delta = spreadMs > 0 ? `+${(offsetMs / 1000).toFixed(3)}s` : ''; return `
${truncate(t.observer || 'β€”', 20)}
${delta}
${t.snr != null ? t.snr.toFixed(1) + ' dB' : 'β€”'}
${t.rssi != null ? t.rssi.toFixed(0) + ' dBm' : 'β€”'}
`; }); return `

Propagation Timeline

ObserverTimeΞ”SNRRSSI
${rows.join('')}
`; } function renderObserverTable() { const rows = traceData.map((t, i) => { const snrClass = t.snr != null ? (t.snr >= 0 ? 'good' : t.snr >= -10 ? 'ok' : 'bad') : ''; return ` ${i + 1} ${t.observer || 'β€”'} ${t.time ? new Date(t.time).toLocaleString() : 'β€”'} ${t.snr != null ? t.snr.toFixed(1) + ' dB' : 'β€”'} ${t.rssi != null ? t.rssi.toFixed(0) + ' dBm' : 'β€”'} `; }); return `

Observer Details

${rows.join('')}
#ObserverTimestampSNRRSSI
`; } registerPage('traces', { init, destroy }); })();