From 532935cb4fe833dfbb730d5f9b76e5d42e9a73f9 Mon Sep 17 00:00:00 2001 From: you Date: Fri, 24 Apr 2026 22:51:39 +0000 Subject: [PATCH] feat(#690): add collapsible evidence panel to node clock skew card Renders recentHashEvidence from the API in a collapsible details section. Each hash shows observer count, median corrected skew, and per-observer breakdown (raw, corrected, observer offset). Includes calibration summary line and plain-English severity reason at top. --- public/nodes.js | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/public/nodes.js b/public/nodes.js index 5d3355e..14200ee 100644 --- a/public/nodes.js +++ b/public/nodes.js @@ -815,6 +815,41 @@ * Shared between the full-screen detail page and the side panel (#813, #690). * No-op if the container is missing, the API errors, or the response lacks severity. */ + /** Build collapsible evidence panel for node clock skew card */ + function buildEvidencePanel(cs) { + var evidence = cs.recentHashEvidence; + if (!evidence || evidence.length === 0) return ''; + + var calSum = cs.calibrationSummary || {}; + var calLine = calSum.totalSamples + ? '
Last ' + calSum.totalSamples + ' samples: ' + (calSum.calibratedSamples || 0) + ' corrected via observer calibration, ' + (calSum.uncalibratedSamples || 0) + ' uncorrected (single-observer).
' + : ''; + + // Severity reason. + var skewVal = window.currentSkewValue(cs); + var sampleCount = (cs.samples || []).length; + var sevLabel = SKEW_SEVERITY_LABELS[cs.severity] || cs.severity; + var reasonLine = '
Recent ' + sampleCount + ' adverts median ' + formatSkew(skewVal) + ' → ' + sevLabel + '
'; + + var hashBlocks = evidence.map(function(ev) { + var shortHash = (ev.hash || '').substring(0, 8) + '…'; + var obsCount = ev.observers ? ev.observers.length : 0; + var header = '
Hash ' + shortHash + ' · ' + obsCount + ' observer' + (obsCount !== 1 ? 's' : '') + ' · median corrected: ' + formatSkew(ev.medianCorrectedSkewSec) + '
'; + var lines = (ev.observers || []).map(function(o) { + var name = o.observerName || o.observerID; + return '
' + + name + ' raw=' + formatSkew(o.rawSkewSec) + ' corrected=' + formatSkew(o.correctedSkewSec) + ' (observer offset ' + formatSkew(o.observerOffsetSec) + ')' + + '
'; + }).join(''); + return header + lines; + }).join(''); + + return '
Evidence (' + evidence.length + ' hashes)' + + '
' + + reasonLine + calLine + hashBlocks + + '
'; + } + async function loadClockSkewInto(container, pubkey) { if (!container) return; try { @@ -841,7 +876,8 @@ '' + driftHtml + (sparkHtml ? '
' + sparkHtml + '
Skew over time (' + (cs.samples || []).length + ' samples)
' : '') + - bimodalWarning; + bimodalWarning + + buildEvidencePanel(cs); } catch (e) { // Non-fatal — section stays hidden }