diff --git a/public/analytics.js b/public/analytics.js
index d2a47e4c..00130471 100644
--- a/public/analytics.js
+++ b/public/analytics.js
@@ -1117,6 +1117,20 @@
try { localStorage.setItem(CHANNEL_SORT_KEY, JSON.stringify(state)); } catch (e) {}
}
+ // The server sends the channel hash byte as a decimal string ("17"); show it
+ // as hex (0x11) like the rest of the UI. Non-numeric ids (e.g. "enc_A1B2")
+ // pass through untouched.
+ function channelHashNum(h) {
+ if (typeof h === 'number') return h;
+ return (typeof h === 'string' && /^\d+$/.test(h)) ? parseInt(h, 10) : NaN;
+ }
+
+ function formatChannelHash(h) {
+ var n = channelHashNum(h);
+ if (!isNaN(n)) return '0x' + n.toString(16).toUpperCase().padStart(2, '0');
+ return h == null ? '' : esc(String(h));
+ }
+
function sortChannels(channels, col, dir) {
var sorted = channels.slice();
var mult = dir === 'asc' ? 1 : -1;
@@ -1127,10 +1141,9 @@
av = (a.name || '').toLowerCase(); bv = (b.name || '').toLowerCase();
return av < bv ? -1 * mult : av > bv ? 1 * mult : 0;
case 'hash':
- av = typeof a.hash === 'number' ? a.hash : String(a.hash);
- bv = typeof b.hash === 'number' ? b.hash : String(b.hash);
- if (typeof av === 'number' && typeof bv === 'number') return (av - bv) * mult;
- av = String(av).toLowerCase(); bv = String(bv).toLowerCase();
+ av = channelHashNum(a.hash); bv = channelHashNum(b.hash);
+ if (!isNaN(av) && !isNaN(bv)) return (av - bv) * mult;
+ av = String(a.hash).toLowerCase(); bv = String(b.hash).toLowerCase();
return av < bv ? -1 * mult : av > bv ? 1 * mult : 0;
case 'messages': return (a.messages - b.messages) * mult;
case 'senders': return (a.senders - b.senders) * mult;
@@ -1158,7 +1171,7 @@
: esc(c.displayName || c.name || 'Unknown');
return '
' +
'| ' + nameHtml + ' | ' +
- '' + (typeof c.hash === 'number' ? '0x' + c.hash.toString(16).toUpperCase().padStart(2, '0') : c.hash) + ' | ' +
+ '' + formatChannelHash(c.hash) + ' | ' +
'' + c.messages + ' | ' +
'' + c.senders + ' | ' +
'' + timeAgo(c.lastActivity) + ' | ' +
diff --git a/tests/unit/test-frontend-helpers.js b/tests/unit/test-frontend-helpers.js
index 00812d53..d409dfbd 100644
--- a/tests/unit/test-frontend-helpers.js
+++ b/tests/unit/test-frontend-helpers.js
@@ -2244,6 +2244,33 @@ console.log('\n=== analytics.js: sortChannels ===');
assert.ok(chatIdx < generalIdx, 'Chat (100 msgs) should come before General (50)');
});
+ // #190: server sends the hash byte as a decimal string; the table must show hex.
+ test('tbodyHtml renders decimal-string hash as hex (#190)', () => {
+ const data = [
+ { name: 'Public', hash: '17', messages: 3, senders: 1, lastActivity: '', encrypted: false },
+ { name: 'Wardriving', hash: '129', messages: 2, senders: 1, lastActivity: '', encrypted: false },
+ { name: 'Low', hash: '5', messages: 1, senders: 1, lastActivity: '', encrypted: false },
+ ];
+ const html = tbodyHtml(data, 'messages', 'desc');
+ assert.ok(html.includes('>0x11<'), 'hash "17" should render as 0x11');
+ assert.ok(html.includes('>0x81<'), 'hash "129" should render as 0x81');
+ assert.ok(html.includes('>0x05<'), 'hash "5" should be zero-padded to 0x05');
+ assert.ok(!html.includes('>17<') && !html.includes('>129<'), 'decimal must not be displayed');
+ assert.ok(html.includes('?ch=17"'), 'deep link keeps the decimal value');
+ });
+
+ test('tbodyHtml leaves non-numeric hash ids untouched and escaped (#190)', () => {
+ const html = tbodyHtml([{ name: 'X', hash: 'enc_A1B2', messages: 1, senders: 1, lastActivity: '', encrypted: true }], 'messages', 'desc');
+ assert.ok(html.includes('>enc_A1B2<'));
+ const evil = tbodyHtml([{ name: 'X', hash: '', messages: 1, senders: 1, lastActivity: '', encrypted: false }], 'messages', 'desc');
+ assert.ok(!evil.includes(''), 'hash must be HTML-escaped');
+ });
+
+ test('sort by hash is numeric for decimal-string hashes (#190)', () => {
+ const data = [{ hash: '129' }, { hash: '17' }, { hash: '5' }];
+ assert.deepStrictEqual(sortChannels(data, 'hash', 'asc').map(c => c.hash), ['5', '17', '129']);
+ });
+
test('sort by string hash values', () => {
const data = [
{ name: 'A', hash: 'zz', messages: 1, senders: 1, lastActivity: '', encrypted: false },
|