fix: display channel hash as hex instead of decimal (#471)

## Summary

Fixes #465 — Channel hash was displaying in decimal instead of
hexadecimal in `channels.js`.

## Changes

- Added `formatHashHex()` helper to `channels.js` that formats numeric
hashes as `0x` hex (e.g. `0x0A`) and passes string hashes through
unchanged
- Applied to both display sites: `renderChannelList` fallback name and
`selectChannel` header text
- Consistent with `packets.js` and `analytics.js` which already use
`.toString(16).padStart(2, '0').toUpperCase()`

## Tests

- 3 new tests in `test-frontend-helpers.js` verifying the helper exists,
is used at display sites, and produces correct output for numeric and
string inputs
- All 244 frontend tests pass, plus packet-filter (62) and aging (29)
tests

Co-authored-by: you <you@example.com>
This commit is contained in:
Kpa-clawbot
2026-04-01 19:45:16 -07:00
committed by GitHub
co-authored by you
parent 623ebc879b
commit c678555e75
3 changed files with 62 additions and 30 deletions
+5 -2
View File
@@ -274,6 +274,9 @@
for (let i = 0; i < str.length; i++) h = ((h << 5) - h + str.charCodeAt(i)) | 0;
return Math.abs(h);
}
function formatHashHex(hash) {
return typeof hash === 'number' ? '0x' + hash.toString(16).toUpperCase().padStart(2, '0') : hash;
}
function getChannelColor(hash) { return CHANNEL_COLORS[hashCode(String(hash)) % CHANNEL_COLORS.length]; }
function getSenderColor(name) {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark' ||
@@ -659,7 +662,7 @@
});
el.innerHTML = sorted.map(ch => {
const name = ch.name || `Channel ${ch.hash}`;
const name = ch.name || `Channel ${formatHashHex(ch.hash)}`;
const color = getChannelColor(ch.hash);
const time = ch.lastActivityMs ? formatSecondsAgo(Math.floor((Date.now() - ch.lastActivityMs) / 1000)) : '';
const preview = ch.lastSender && ch.lastMessage
@@ -688,7 +691,7 @@
history.replaceState(null, '', `#/channels/${encodeURIComponent(hash)}`);
renderChannelList();
const ch = channels.find(c => c.hash === hash);
const name = ch?.name || `Channel ${hash}`;
const name = ch?.name || `Channel ${formatHashHex(hash)}`;
const header = document.getElementById('chHeader');
header.querySelector('.ch-header-text').textContent = `${name}${ch?.messageCount || 0} messages`;
+28 -28
View File
@@ -22,9 +22,9 @@
<meta name="twitter:title" content="CoreScope">
<meta name="twitter:description" content="Real-time MeshCore LoRa mesh network analyzer — live packet visualization, node tracking, channel decryption, and route analysis.">
<meta name="twitter:image" content="https://raw.githubusercontent.com/Kpa-clawbot/corescope/master/public/og-image.png">
<link rel="stylesheet" href="style.css?v=1775096024">
<link rel="stylesheet" href="home.css?v=1775096024">
<link rel="stylesheet" href="live.css?v=1775096024">
<link rel="stylesheet" href="style.css?v=1775097178">
<link rel="stylesheet" href="home.css?v=1775097178">
<link rel="stylesheet" href="live.css?v=1775097178">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin="anonymous">
@@ -85,30 +85,30 @@
<main id="app" role="main"></main>
<script src="vendor/qrcode.js"></script>
<script src="roles.js?v=1775096024"></script>
<script src="customize.js?v=1775096024" onerror="console.error('Failed to load:', this.src)"></script>
<script src="region-filter.js?v=1775096024"></script>
<script src="hop-resolver.js?v=1775096024"></script>
<script src="hop-display.js?v=1775096024"></script>
<script src="app.js?v=1775096024"></script>
<script src="home.js?v=1775096024"></script>
<script src="packet-filter.js?v=1775096024"></script>
<script src="packets.js?v=1775096024"></script>
<script src="geo-filter-overlay.js?v=1775096024"></script>
<script src="map.js?v=1775096024" onerror="console.error('Failed to load:', this.src)"></script>
<script src="channels.js?v=1775096024" onerror="console.error('Failed to load:', this.src)"></script>
<script src="nodes.js?v=1775096024" onerror="console.error('Failed to load:', this.src)"></script>
<script src="traces.js?v=1775096024" onerror="console.error('Failed to load:', this.src)"></script>
<script src="analytics.js?v=1775096024" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio.js?v=1775096024" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v1-constellation.js?v=1775096024" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v2-constellation.js?v=1775096024" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-lab.js?v=1775096024" onerror="console.error('Failed to load:', this.src)"></script>
<script src="live.js?v=1775096024" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observers.js?v=1775096024" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observer-detail.js?v=1775096024" onerror="console.error('Failed to load:', this.src)"></script>
<script src="compare.js?v=1775096024" onerror="console.error('Failed to load:', this.src)"></script>
<script src="node-analytics.js?v=1775096024" onerror="console.error('Failed to load:', this.src)"></script>
<script src="perf.js?v=1775096024" onerror="console.error('Failed to load:', this.src)"></script>
<script src="roles.js?v=1775097178"></script>
<script src="customize.js?v=1775097178" onerror="console.error('Failed to load:', this.src)"></script>
<script src="region-filter.js?v=1775097178"></script>
<script src="hop-resolver.js?v=1775097178"></script>
<script src="hop-display.js?v=1775097178"></script>
<script src="app.js?v=1775097178"></script>
<script src="home.js?v=1775097178"></script>
<script src="packet-filter.js?v=1775097178"></script>
<script src="packets.js?v=1775097178"></script>
<script src="geo-filter-overlay.js?v=1775097178"></script>
<script src="map.js?v=1775097178" onerror="console.error('Failed to load:', this.src)"></script>
<script src="channels.js?v=1775097178" onerror="console.error('Failed to load:', this.src)"></script>
<script src="nodes.js?v=1775097178" onerror="console.error('Failed to load:', this.src)"></script>
<script src="traces.js?v=1775097178" onerror="console.error('Failed to load:', this.src)"></script>
<script src="analytics.js?v=1775097178" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio.js?v=1775097178" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v1-constellation.js?v=1775097178" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v2-constellation.js?v=1775097178" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-lab.js?v=1775097178" onerror="console.error('Failed to load:', this.src)"></script>
<script src="live.js?v=1775097178" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observers.js?v=1775097178" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observer-detail.js?v=1775097178" onerror="console.error('Failed to load:', this.src)"></script>
<script src="compare.js?v=1775097178" onerror="console.error('Failed to load:', this.src)"></script>
<script src="node-analytics.js?v=1775097178" onerror="console.error('Failed to load:', this.src)"></script>
<script src="perf.js?v=1775097178" onerror="console.error('Failed to load:', this.src)"></script>
</body>
</html>
+29
View File
@@ -2876,6 +2876,35 @@ console.log('\n=== live.js: nextHop null guards ===');
});
}
// === channels.js: formatHashHex (#465) ===
console.log('\n=== channels.js: formatHashHex (issue #465) ===');
{
const chSource = fs.readFileSync('public/channels.js', 'utf8');
test('formatHashHex exists in channels.js', () => {
assert.ok(chSource.includes('function formatHashHex('), 'formatHashHex function must exist');
});
test('channel fallback name uses formatHashHex', () => {
assert.ok(chSource.includes('formatHashHex(ch.hash)'), 'renderChannelList must format hash as hex');
assert.ok(chSource.includes('formatHashHex(hash)'), 'selectChannel must format hash as hex');
});
test('formatHashHex produces correct hex output', () => {
// Extract and evaluate the function
const match = chSource.match(/function formatHashHex\(hash\)\s*\{[^}]+\}/);
assert.ok(match, 'should extract formatHashHex');
const ctx = vm.createContext({});
vm.runInContext(match[0], ctx);
const fmt = vm.runInContext('formatHashHex', ctx);
assert.strictEqual(fmt(10), '0x0A');
assert.strictEqual(fmt(255), '0xFF');
assert.strictEqual(fmt(0), '0x00');
assert.strictEqual(fmt(1), '0x01');
assert.strictEqual(fmt('LongFast'), 'LongFast'); // string hash passes through
});
}
// ===== SUMMARY =====
Promise.allSettled(pendingTests).then(() => {
console.log(`\n${'═'.repeat(40)}`);