,
+ // srcResolvedName, destResolvedName, observedHops, observationCount }
+ function buildPacketContextBlock(pktCtx) {
+ if (!pktCtx || !pktCtx.type) return '';
+ var t = pktCtx.type;
+ var d = pktCtx.decoded || {};
+ var glyph, label, factsHtml = '';
+ switch (t) {
+ case 'ADVERT':
+ glyph = '๐ก'; label = 'ADVERT';
+ var name = d.adName || d.name || (d.pubKey ? d.pubKey.slice(0, 8) : '?');
+ var role = (d.flags && (d.flags.repeater ? 'repeater' : d.flags.room ? 'room' : d.flags.sensor ? 'sensor' : d.flags.chat ? 'companion' : 'unknown')) || 'unknown';
+ // no fabricated fields. Battery isn't decoded into the advert
+ // JSON โ adverts carry lat/lon + name + flags, not battery. If a
+ // future advert version exposes it, re-add then.
+ var sig = (d.signatureValid === true) ? 'โ' : (d.signatureValid === false ? 'โ' : null);
+ var line1 = '' + escapeHtml(name) + ' ยท ' + escapeHtml(role);
+ if (sig) line1 += ' ยท sig ' + sig;
+ // Self-reported GPS if present
+ if (d.lat != null && d.lon != null) {
+ line1 += ' ยท ' + d.lat.toFixed(3) + ', ' + d.lon.toFixed(3);
+ }
+ var pkPrefix = d.pubKey ? d.pubKey.slice(0, 12) + 'โฆ' : '';
+ factsHtml = '' + line1 + '
';
+ if (pkPrefix) factsHtml += '' + escapeHtml(pkPrefix) + '
';
+ break;
+ case 'PATH':
+ glyph = '๐'; label = 'PATH';
+ var psrc = pktCtx.srcResolvedName || (d.srcHash ? 'unknown (hash ' + d.srcHash + ')' : '?');
+ var pdst = pktCtx.destResolvedName || (d.destHash ? 'unknown (hash ' + d.destHash + ')' : '?');
+ factsHtml = '' + escapeHtml(psrc) + ' โ ' + escapeHtml(pdst) + '
';
+ break;
+ case 'TXT_MSG':
+ case 'REQ':
+ case 'RESPONSE':
+ case 'ANON_REQ':
+ var typeGlyphs = { 'TXT_MSG': 'โ', 'REQ': '๐', 'RESPONSE': '๐', 'ANON_REQ': '๐' };
+ var typeLabels = { 'TXT_MSG': 'DM', 'REQ': 'REQUEST', 'RESPONSE': 'RESPONSE', 'ANON_REQ': 'ANON REQ' };
+ glyph = typeGlyphs[t] || 'ยท';
+ label = typeLabels[t] || t;
+ var src = pktCtx.srcResolvedName || (d.srcHash ? 'unknown (hash ' + d.srcHash + ')' : (t === 'ANON_REQ' ? 'anon' : '?'));
+ var dst = pktCtx.destResolvedName || (d.destHash ? 'unknown (hash ' + d.destHash + ')' : '?');
+ factsHtml = '' + escapeHtml(src) + ' โ ' + escapeHtml(dst) + '
';
+ factsHtml += '๐ encrypted
';
+ break;
+ case 'GRP_TXT':
+ case 'CHAN':
+ glyph = '#'; label = 'CHANNEL MSG';
+ var chName = pktCtx.channelName || d.channel || (d.channelHashHex ? 'channel 0x' + d.channelHashHex : 'channel ?');
+ var contentText = pktCtx.decryptedText || d.text || d.plainText || null;
+ var encStatus = contentText ? '๐ decrypted' : (d.decryptionStatus === 'decrypted' ? '๐ decrypted' : '๐ no key');
+ factsHtml = '' + escapeHtml(chName) + '
';
+ factsHtml += '' + encStatus + '
';
+ if (contentText) {
+ var preview = contentText.slice(0, 80);
+ if (contentText.length > 80) preview += 'โฆ';
+ factsHtml += '"' + escapeHtml(preview) + '"
';
+ }
+ var senderName = pktCtx.srcResolvedName || d.sender || (d.srcHash ? 'sender 0x' + d.srcHash : null);
+ if (senderName) factsHtml += 'from ' + escapeHtml(senderName) + '
';
+ break;
+ case 'TRACE':
+ glyph = 'โ'; label = 'TRACE';
+ var officialHops = (d.routeTaken && d.routeTaken.length) || (d.route && d.route.length) || null;
+ var observed = (pktCtx.observedHops != null) ? pktCtx.observedHops : null;
+ if (officialHops != null && observed != null) {
+ factsHtml = 'Official: ' + officialHops + ' hops ยท Observed: ' + observed + '
';
+ } else if (officialHops != null) {
+ factsHtml = 'Official route: ' + officialHops + ' hops
';
+ }
+ if (pktCtx.issuedBy) factsHtml += 'issued by ' + escapeHtml(pktCtx.issuedBy) + '
';
+ break;
+ default:
+ glyph = 'ยท'; label = (t || 'OTHER').toUpperCase();
+ if (pktCtx.payloadSize != null) {
+ factsHtml = '' + pktCtx.payloadSize + ' bytes
';
+ }
+ break;
+ }
+ return '' +
+ '
' + glyph + ' ' + escapeHtml(label) + '
' +
+ '
' + factsHtml + '
' +
+ '
';
+ }
+
+ function buildSnrSparkline(snrTrend) {
+ if (!snrTrend || !snrTrend.length) return 'no SNR data ';
+ var pts = snrTrend.filter(function (p) { return p && p.snr != null; });
+ if (!pts.length) return 'no SNR data ';
+ var W = 200, H = 28;
+ var snrs = pts.map(function (p) { return p.snr; });
+ var minS = Math.min.apply(null, snrs), maxS = Math.max.apply(null, snrs);
+ if (maxS === minS) { minS -= 1; maxS += 1; }
+ // n<3 is not a sparkline โ a 2-point polyline implies a
+ // trend across time it can't represent. Show DOTS only (no connecting line)
+ // when there are fewer than 3 observations.
+ var showLine = pts.length >= 3;
+ var poly = pts.map(function (p, i) {
+ var x = (i / (pts.length - 1 || 1)) * W;
+ var y = H - 2 - ((p.snr - minS) / (maxS - minS)) * (H - 4);
+ return x.toFixed(1) + ',' + y.toFixed(1);
+ }).join(' ');
+ var svg = '';
+ if (showLine) {
+ svg += ' ';
+ }
+ // Dots always (data points themselves are the truth)
+ pts.forEach(function (p, i) {
+ var x = (i / (pts.length - 1 || 1)) * W;
+ var y = H - 2 - ((p.snr - minS) / (maxS - minS)) * (H - 4);
+ svg += ' ';
+ });
+ svg += ' ';
+ return svg +
+ '' + pts.length + ' obs ยท ' + minS.toFixed(1) + '..' + maxS.toFixed(1) + ' dB ';
+ }
+
+ window.MC_ROUTE_UTILS = {
+ escapeHtml: escapeHtml,
+ buildPacketContextBlock: buildPacketContextBlock,
+ buildSnrSparkline: buildSnrSparkline
+ };
+})();
diff --git a/public/route-view.js b/public/route-view.js
index 94c8eef8..329e71b9 100644
--- a/public/route-view.js
+++ b/public/route-view.js
@@ -72,132 +72,14 @@
return Math.round(d/86400000) + 'd ago';
}
- function escapeHtml(s) {
- return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
- return ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c];
- });
- }
-
- // packet-context block (one stable layout, type chip + 3-5
- // facts). pktCtx shape:
- // { type: 'ADVERT'|'TXT_MSG'|'GRP_TXT'|'TRACE'|other,
- // decoded: ,
- // payloadType: ,
- // srcResolvedName, destResolvedName, observedHops, observationCount }
- function buildPacketContextBlock(pktCtx) {
- if (!pktCtx || !pktCtx.type) return '';
- var t = pktCtx.type;
- var d = pktCtx.decoded || {};
- var glyph, label, factsHtml = '';
- switch (t) {
- case 'ADVERT':
- glyph = '๐ก'; label = 'ADVERT';
- var name = d.adName || d.name || (d.pubKey ? d.pubKey.slice(0, 8) : '?');
- var role = (d.flags && (d.flags.repeater ? 'repeater' : d.flags.room ? 'room' : d.flags.sensor ? 'sensor' : d.flags.chat ? 'companion' : 'unknown')) || 'unknown';
- // no fabricated fields. Battery isn't decoded into the advert
- // JSON โ adverts carry lat/lon + name + flags, not battery. If a
- // future advert version exposes it, re-add then.
- var sig = (d.signatureValid === true) ? 'โ' : (d.signatureValid === false ? 'โ' : null);
- var line1 = '' + escapeHtml(name) + ' ยท ' + escapeHtml(role);
- if (sig) line1 += ' ยท sig ' + sig;
- // Self-reported GPS if present
- if (d.lat != null && d.lon != null) {
- line1 += ' ยท ' + d.lat.toFixed(3) + ', ' + d.lon.toFixed(3);
- }
- var pkPrefix = d.pubKey ? d.pubKey.slice(0, 12) + 'โฆ' : '';
- factsHtml = '' + line1 + '
';
- if (pkPrefix) factsHtml += '' + escapeHtml(pkPrefix) + '
';
- break;
- case 'PATH':
- glyph = '๐'; label = 'PATH';
- var psrc = pktCtx.srcResolvedName || (d.srcHash ? 'unknown (hash ' + d.srcHash + ')' : '?');
- var pdst = pktCtx.destResolvedName || (d.destHash ? 'unknown (hash ' + d.destHash + ')' : '?');
- factsHtml = '' + escapeHtml(psrc) + ' โ ' + escapeHtml(pdst) + '
';
- break;
- case 'TXT_MSG':
- case 'REQ':
- case 'RESPONSE':
- case 'ANON_REQ':
- var typeGlyphs = { 'TXT_MSG': 'โ', 'REQ': '๐', 'RESPONSE': '๐', 'ANON_REQ': '๐' };
- var typeLabels = { 'TXT_MSG': 'DM', 'REQ': 'REQUEST', 'RESPONSE': 'RESPONSE', 'ANON_REQ': 'ANON REQ' };
- glyph = typeGlyphs[t] || 'ยท';
- label = typeLabels[t] || t;
- var src = pktCtx.srcResolvedName || (d.srcHash ? 'unknown (hash ' + d.srcHash + ')' : (t === 'ANON_REQ' ? 'anon' : '?'));
- var dst = pktCtx.destResolvedName || (d.destHash ? 'unknown (hash ' + d.destHash + ')' : '?');
- factsHtml = '' + escapeHtml(src) + ' โ ' + escapeHtml(dst) + '
';
- factsHtml += '๐ encrypted
';
- break;
- case 'GRP_TXT':
- case 'CHAN':
- glyph = '#'; label = 'CHANNEL MSG';
- var chName = pktCtx.channelName || d.channel || (d.channelHashHex ? 'channel 0x' + d.channelHashHex : 'channel ?');
- var contentText = pktCtx.decryptedText || d.text || d.plainText || null;
- var encStatus = contentText ? '๐ decrypted' : (d.decryptionStatus === 'decrypted' ? '๐ decrypted' : '๐ no key');
- factsHtml = '' + escapeHtml(chName) + '
';
- factsHtml += '' + encStatus + '
';
- if (contentText) {
- var preview = contentText.slice(0, 80);
- if (contentText.length > 80) preview += 'โฆ';
- factsHtml += '"' + escapeHtml(preview) + '"
';
- }
- var senderName = pktCtx.srcResolvedName || d.sender || (d.srcHash ? 'sender 0x' + d.srcHash : null);
- if (senderName) factsHtml += 'from ' + escapeHtml(senderName) + '
';
- break;
- case 'TRACE':
- glyph = 'โ'; label = 'TRACE';
- var officialHops = (d.routeTaken && d.routeTaken.length) || (d.route && d.route.length) || null;
- var observed = (pktCtx.observedHops != null) ? pktCtx.observedHops : null;
- if (officialHops != null && observed != null) {
- factsHtml = 'Official: ' + officialHops + ' hops ยท Observed: ' + observed + '
';
- } else if (officialHops != null) {
- factsHtml = 'Official route: ' + officialHops + ' hops
';
- }
- if (pktCtx.issuedBy) factsHtml += 'issued by ' + escapeHtml(pktCtx.issuedBy) + '
';
- break;
- default:
- glyph = 'ยท'; label = (t || 'OTHER').toUpperCase();
- if (pktCtx.payloadSize != null) {
- factsHtml = '' + pktCtx.payloadSize + ' bytes
';
- }
- break;
- }
- return '' +
- '
' + glyph + ' ' + escapeHtml(label) + '
' +
- '
' + factsHtml + '
' +
- '
';
- }
-
- function buildSnrSparkline(snrTrend) {
- if (!snrTrend || !snrTrend.length) return 'no SNR data ';
- var pts = snrTrend.filter(function (p) { return p && p.snr != null; });
- if (!pts.length) return 'no SNR data ';
- var W = 200, H = 28;
- var snrs = pts.map(function (p) { return p.snr; });
- var minS = Math.min.apply(null, snrs), maxS = Math.max.apply(null, snrs);
- if (maxS === minS) { minS -= 1; maxS += 1; }
- // n<3 is not a sparkline โ a 2-point polyline implies a
- // trend across time it can't represent. Show DOTS only (no connecting line)
- // when there are fewer than 3 observations.
- var showLine = pts.length >= 3;
- var poly = pts.map(function (p, i) {
- var x = (i / (pts.length - 1 || 1)) * W;
- var y = H - 2 - ((p.snr - minS) / (maxS - minS)) * (H - 4);
- return x.toFixed(1) + ',' + y.toFixed(1);
- }).join(' ');
- var svg = '';
- if (showLine) {
- svg += ' ';
- }
- // Dots always (data points themselves are the truth)
- pts.forEach(function (p, i) {
- var x = (i / (pts.length - 1 || 1)) * W;
- var y = H - 2 - ((p.snr - minS) / (maxS - minS)) * (H - 4);
- svg += ' ';
- });
- svg += ' ';
- return svg +
- '' + pts.length + ' obs ยท ' + minS.toFixed(1) + '..' + maxS.toFixed(1) + ' dB ';
- }
+ // #1424 โ pure helpers (escapeHtml, buildPacketContextBlock,
+ // buildSnrSparkline) extracted into public/route-view-utils.js. Loader
+ // for that file MUST run before route-view.js (see index.html). Local
+ // refs keep the call sites in this file unchanged.
+ var _MC_RT_U = window.MC_ROUTE_UTILS || {};
+ var escapeHtml = _MC_RT_U.escapeHtml;
+ var buildPacketContextBlock = _MC_RT_U.buildPacketContextBlock;
+ var buildSnrSparkline = _MC_RT_U.buildSnrSparkline;
// Polish review (carmack #1423): bound _detailCache (was unbounded plain
// object; every distinct pubkey ever clicked was retained for the tab's
@@ -1369,6 +1251,9 @@
// Spider-fan: after Leaflet projects, group any markers within
// 25px of each other and offset them on an arc around their centroid.
// Draw a hairline from each offset marker back to the centroid.
+ // #1424: NOT extracted into route-view-utils.js โ this consumes Leaflet
+ // types (mapRef.latLngToLayerPoint, mk.getLatLng / setLatLng, L.point)
+ // and mutates marker objects, so it isn't pure.
function spiderFanFor(markerArray, positionArray) {
if (!mapRef || !mapRef.latLngToLayerPoint) return;
var pts = markerArray.map(function (mk, i) {