/* === 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
${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' : 'β'}
`;
});
return `
Propagation Timeline
${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
| # | Observer | Timestamp | SNR | RSSI |
${rows.join('')}
`;
}
registerPage('traces', { init, destroy });
})();