From 1d449eabc79c4a0dfbc216bff9cdceec07973255 Mon Sep 17 00:00:00 2001 From: Kpa-clawbot Date: Tue, 21 Apr 2026 10:54:32 -0700 Subject: [PATCH] fix(#872): replace strikethrough with warning badge on unreliable hops (#875) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The `hop-unreliable` CSS class applied `text-decoration: line-through` and `opacity: 0.5`, making hop names look "dead" to operators. This caused confusion — the repeater itself is fine, only the name→hash assignment is uncertain. ## Fix - **CSS**: Removed `line-through` and heavy opacity from `.hop-unreliable`. Kept subtle `opacity: 0.85` for scanability. Added `.hop-unreliable-btn` style for the new badge. - **JS**: Added a `⚠️` warning badge button next to unreliable hops (similar pattern to existing conflict badges). The badge is always visible, keyboard-focusable, and has both `title` and `aria-label` with an informative tooltip explaining geographic inconsistency. - **Tests**: Added 2 tests in `test-frontend-helpers.js` asserting the badge renders for unreliable hops and does NOT render for reliable ones, and that no `line-through` is present. ### Before → After | Before | After | |--------|-------| | ~~NodeName~~ (struck through, 50% opacity) | NodeName ⚠️ (normal text, small warning badge with tooltip) | ## Scope Resolver logic untouched — #873 covers threshold tuning, #874 covers picker correctness. No candidate-dropdown UX (follow-up per issue discussion). Closes #872 Co-authored-by: you --- public/hop-display.js | 6 ++++- public/style.css | 4 ++- test-frontend-helpers.js | 56 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/public/hop-display.js b/public/hop-display.js index 543582ec..86da8021 100644 --- a/public/hop-display.js +++ b/public/hop-display.js @@ -81,9 +81,13 @@ window.HopDisplay = (function() { const regionalConflicts = conflicts.filter(c => c.regional); const badgeCount = regionalConflicts.length > 0 ? regionalConflicts.length : (globalFallback ? conflicts.length : 0); const conflictData = escapeHtml(JSON.stringify({ h, conflicts, globalFallback })); - const warnBadge = badgeCount > 1 + const conflictBadge = badgeCount > 1 ? ` ` : ''; + const unreliableBadge = unreliable + ? ' ' + : ''; + const warnBadge = conflictBadge + unreliableBadge; const cls = [ 'hop', diff --git a/public/style.css b/public/style.css index 70f6181c..db9c5d72 100644 --- a/public/style.css +++ b/public/style.css @@ -1437,7 +1437,9 @@ button.ch-item.ch-item-encrypted .ch-badge { filter: grayscale(0.6); } .hop-conflict-name { font-weight: 600; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .hop-conflict-dist { font-size: 11px; color: var(--text-muted); font-family: var(--mono); white-space: nowrap; } .hop-conflict-pk { font-size: 10px; color: var(--text-muted); font-family: var(--mono); } -.hop-unreliable { opacity: 0.5; text-decoration: line-through; } +.hop-unreliable { opacity: 0.85; } +.hop-unreliable-btn { background: none; border: none; color: var(--status-yellow, #f59e0b); font-size: 13px; + cursor: help; vertical-align: middle; margin-left: 2px; padding: 0 2px; line-height: 1; } .hop-global-fallback { border-bottom: 1px dashed var(--status-red); } .hop-current { font-weight: 700 !important; color: var(--accent) !important; } diff --git a/test-frontend-helpers.js b/test-frontend-helpers.js index 8327df5e..3c06ba9d 100644 --- a/test-frontend-helpers.js +++ b/test-frontend-helpers.js @@ -6198,6 +6198,62 @@ console.log('\n=== analytics.js: renderCollisionsFromServer collision table ===' }); } +// ===== #872 — hop-display unreliable badge ===== +{ + console.log('\n--- #872: hop-display unreliable warning badge ---'); + + function makeHopDisplaySandbox() { + const sb = { + window: { addEventListener: () => {}, dispatchEvent: () => {} }, + document: { + readyState: 'complete', + createElement: () => ({ id: '', textContent: '', innerHTML: '' }), + head: { appendChild: () => {} }, + getElementById: () => null, + addEventListener: () => {}, + querySelectorAll: () => [], + querySelector: () => null, + }, + console, + Date, Math, Array, Object, String, Number, JSON, RegExp, Map, Set, + encodeURIComponent, parseInt, parseFloat, isNaN, Infinity, NaN, undefined, + setTimeout: () => {}, setInterval: () => {}, clearTimeout: () => {}, clearInterval: () => {}, + }; + sb.window.document = sb.document; + sb.self = sb.window; + sb.globalThis = sb.window; + const ctx = vm.createContext(sb); + const hopSrc = fs.readFileSync(__dirname + '/public/hop-display.js', 'utf8'); + vm.runInContext(hopSrc, ctx); + return ctx; + } + + const hopCtx = makeHopDisplaySandbox(); + + test('#872: unreliable hop renders warning badge, not strikethrough', () => { + const html = hopCtx.window.HopDisplay.renderHop('AABB', { + name: 'TestNode', pubkey: 'pk123', unreliable: true, + ambiguous: false, conflicts: [], globalFallback: false, + }, {}); + // Must contain unreliable warning badge button + assert.ok(html.includes('hop-unreliable-btn'), 'should have unreliable badge button'); + assert.ok(html.includes('⚠️'), 'should have ⚠️ icon'); + assert.ok(html.includes('Unreliable name resolution'), 'should have tooltip text'); + // Must NOT contain line-through in inline style (CSS class no longer has it) + assert.ok(!html.includes('line-through'), 'should not contain line-through'); + // Should still have hop-unreliable class for subtle styling + assert.ok(html.includes('hop-unreliable'), 'should have hop-unreliable class'); + }); + + test('#872: reliable hop does NOT render unreliable badge', () => { + const html = hopCtx.window.HopDisplay.renderHop('CCDD', { + name: 'GoodNode', pubkey: 'pk456', unreliable: false, + ambiguous: false, conflicts: [], globalFallback: false, + }, {}); + assert.ok(!html.includes('hop-unreliable-btn'), 'should not have unreliable badge'); + }); +} + // ===== SUMMARY ===== Promise.allSettled(pendingTests).then(() => { console.log(`\n${'═'.repeat(40)}`);