feat(pathanalyzer): path hash size (HB) column and filter

- HB column in the message table: distinct hash sizes (bytes per hop)
  across the message's routed echoes, e.g. '1' or '1,2'; em dash when
  no routed echo
- HB filter (Any/1-byte/2-byte/3-byte): matches when any routed echo
  uses that hash size; 0-hop echoes are excluded since their stored
  hash_size is meaningless; combines with the other filters and is
  reset by Clear

Verified live via Playwright: 1B/2B/3B filters each return only
messages with a matching routed echo (183/166/9 of 606, zero false
positives), combined hops+HB filtering works, Clear resets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-07-19 19:19:00 +02:00
parent dca3d58232
commit 67b405251b
2 changed files with 27 additions and 3 deletions
+19 -3
View File
@@ -83,7 +83,7 @@ function showNotification(message, type = 'info') {
// ================================================================
let paMessages = [];
let paFilters = { hops: 'any', token: '', sender: '' };
let paFilters = { hops: 'any', hashSize: 'any', token: '', sender: '' };
let paCurrentView = 'messages'; // 'messages' | 'stats'
let paContacts = []; // /api/contacts/cached?format=full
let paStatsSort = { key: 'relayed', dir: -1 };
@@ -126,6 +126,12 @@ function paMessageMatchesFilters(msg) {
want === '4+' ? e.hops >= 4 : e.hops === parseInt(want, 10));
if (!ok) return false;
}
if (paFilters.hashSize !== 'any') {
const want = parseInt(paFilters.hashSize, 10);
// Only routed echoes carry a meaningful hash size
const ok = msg.echoView.some(e => e.hops > 0 && (e.hash_size || 1) === want);
if (!ok) return false;
}
if (paFilters.token) {
const t = paFilters.token;
const ok = msg.echoView.some(e => e.tokens.some(tok => tok.startsWith(t)));
@@ -138,7 +144,8 @@ function paMessageMatchesFilters(msg) {
}
function paFiltersActive() {
return paFilters.hops !== 'any' || paFilters.token !== '' || paFilters.sender !== '';
return paFilters.hops !== 'any' || paFilters.hashSize !== 'any'
|| paFilters.token !== '' || paFilters.sender !== '';
}
function paFormatTime(msg) {
@@ -258,6 +265,12 @@ function paRenderTable() {
tdHops.textContent = (msg.hop_count === null || msg.hop_count === undefined) ? '—' : msg.hop_count;
tr.appendChild(tdHops);
const tdHb = document.createElement('td');
tdHb.className = 'text-end';
const hbSizes = [...new Set(msg.echoView.filter(e => e.hops > 0).map(e => e.hash_size || 1))].sort();
tdHb.textContent = hbSizes.length > 0 ? hbSizes.join(',') : '—';
tr.appendChild(tdHb);
const tdEchoes = document.createElement('td');
tdEchoes.className = 'text-end';
tdEchoes.textContent = msg.echoView.length;
@@ -270,7 +283,7 @@ function paRenderTable() {
detailTr.className = 'd-none';
const detailTd = document.createElement('td');
detailTd.className = 'pa-echo-cell';
detailTd.colSpan = 8;
detailTd.colSpan = 9;
for (const echo of msg.echoView) {
detailTd.appendChild(paBuildEchoLine(echo));
}
@@ -699,6 +712,7 @@ async function paLoadMessages() {
function paReadFilters() {
paFilters.hops = document.getElementById('paHopsFilter').value;
paFilters.hashSize = document.getElementById('paHashSizeFilter').value;
// Normalize token input to hex characters only (matches chip display casing)
paFilters.token = document.getElementById('paTokenFilter').value
.replace(/[^0-9a-fA-F]/g, '').toUpperCase();
@@ -715,6 +729,7 @@ function paApplyFilters() {
function paClearFilters() {
document.getElementById('paHopsFilter').value = 'any';
document.getElementById('paHashSizeFilter').value = 'any';
document.getElementById('paTokenFilter').value = '';
document.getElementById('paSenderFilter').value = '';
paApplyFilters();
@@ -735,6 +750,7 @@ document.addEventListener('DOMContentLoaded', () => {
filterDebounce = setTimeout(paApplyFilters, 150);
};
document.getElementById('paHopsFilter').addEventListener('change', paApplyFilters);
document.getElementById('paHashSizeFilter').addEventListener('change', paApplyFilters);
document.getElementById('paTokenFilter').addEventListener('input', debouncedApply);
document.getElementById('paSenderFilter').addEventListener('input', debouncedApply);
document.getElementById('paClearFiltersBtn').addEventListener('click', paClearFilters);
+8
View File
@@ -344,6 +344,13 @@
<option value="3">3 hops</option>
<option value="4+">4+ hops</option>
</select>
<select id="paHashSizeFilter" class="form-select form-select-sm" style="width: auto;"
title="Filter by path hash size (bytes per hop, any routed echo)">
<option value="any" selected>Any HB</option>
<option value="1">1-byte</option>
<option value="2">2-byte</option>
<option value="3">3-byte</option>
</select>
<input id="paTokenFilter" type="text" class="form-control form-control-sm" style="width: 9.5rem;"
placeholder="Repeater hash (3B)" title="Match a repeater hash anywhere in a path (prefix match)">
<input id="paSenderFilter" type="text" class="form-control form-control-sm" style="width: 9.5rem;"
@@ -375,6 +382,7 @@
<th>Message</th>
<th>Hash</th>
<th class="text-end">Hops</th>
<th class="text-end" title="Path hash size in bytes per hop (from routed echoes)">HB</th>
<th class="text-end">Echoes</th>
</tr>
</thead>