Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 113 additions & 6 deletions crates/net/rpc/static/fork_choice.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,28 @@
gap: 2px;
}

#follow-head-toggle {
margin-left: auto;
padding: 7px 12px;
border: 1px solid #ff9800;
border-radius: 4px;
background: transparent;
color: #ff9800;
font-family: "Courier New", Courier, monospace;
font-size: 12px;
font-weight: bold;
cursor: pointer;
}

#follow-head-toggle:hover {
background: rgba(255, 152, 0, 0.12);
}

#follow-head-toggle[aria-pressed="false"] {
border-color: #666;
color: #aaa;
}

.value-head { color: #ff9800; }
.value-justified { color: #2196f3; }
.value-finalized { color: #4caf50; }
Expand Down Expand Up @@ -157,6 +179,7 @@
<span class="label">Validators</span>
<span class="value value-validators" id="validator-count">--</span>
</div>
<button id="follow-head-toggle" type="button" aria-pressed="true">Following Head</button>
</div>

<div id="chart-container">
Expand Down Expand Up @@ -185,9 +208,15 @@
const container = document.getElementById("chart-container");
const tooltip = document.getElementById("tooltip");
const emptyMsg = document.getElementById("empty-message");
const followHeadToggle = document.getElementById("follow-head-toggle");

let svg, gLinks, gNodes, gAxis;
let currentData = null;
let followHead = true;
let currentHeadScrollTarget = null;
let lastScrollTop = 0;
let programmaticScrollTimeout = null;
let isProgrammaticScroll = false;

function initSVG() {
svg = d3.select("#chart-container")
Expand Down Expand Up @@ -359,6 +388,50 @@
tooltip.style.opacity = 0;
}

function updateFollowHeadToggle() {
followHeadToggle.setAttribute("aria-pressed", String(followHead));
followHeadToggle.textContent = followHead ? "Following Head" : "Follow Head";
}

function markProgrammaticScroll() {
isProgrammaticScroll = true;
if (programmaticScrollTimeout) clearTimeout(programmaticScrollTimeout);
programmaticScrollTimeout = setTimeout(() => {
isProgrammaticScroll = false;
}, TRANSITION_DURATION + 150);
}

Comment on lines +399 to +403
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Heuristic timeout may misclassify browser scroll events

isProgrammaticScroll is reset after TRANSITION_DURATION + 150 ms (650 ms), but CSS smooth-scroll duration is browser-controlled and not guaranteed to match TRANSITION_DURATION. On slow devices or with large scroll distances, the animation can easily exceed 650 ms, causing the tail-end scroll events to be treated as user-initiated. If those events happen to reduce scrollTop (e.g. scroll target is above current position), setFollowHead(false) could fire spuriously while the programmatic scroll is still completing.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/net/rpc/static/fork_choice.html
Line: 399-403

Comment:
**Heuristic timeout may misclassify browser scroll events**

`isProgrammaticScroll` is reset after `TRANSITION_DURATION + 150` ms (650 ms), but CSS smooth-scroll duration is browser-controlled and not guaranteed to match `TRANSITION_DURATION`. On slow devices or with large scroll distances, the animation can easily exceed 650 ms, causing the tail-end scroll events to be treated as user-initiated. If those events happen to reduce `scrollTop` (e.g. scroll target is above current position), `setFollowHead(false)` could fire spuriously while the programmatic scroll is still completing.

How can I resolve this? If you propose a fix, please make it concise.

function scrollToHead(behavior = "smooth") {
if (currentHeadScrollTarget == null) return;
markProgrammaticScroll();
container.scrollTo({
top: currentHeadScrollTarget,
behavior
});
}

function isAtHead() {
return currentHeadScrollTarget != null &&
Math.abs(container.scrollTop - currentHeadScrollTarget) <= NODE_RADIUS * 3;
}

function setFollowHead(enabled, shouldScroll = false) {
followHead = enabled;
updateFollowHeadToggle();

if (followHead && shouldScroll) {
scrollToHead();
}
}

function markUserScrollIntent() {
isProgrammaticScroll = false;
if (programmaticScrollTimeout) {
clearTimeout(programmaticScrollTimeout);
programmaticScrollTimeout = null;
}
}

function render(data) {
if (!data || !data.nodes || data.nodes.length === 0) {
emptyMsg.style.display = "block";
Expand Down Expand Up @@ -516,16 +589,22 @@
if (hovered) tooltip.innerHTML = tooltipHtml(hovered, data.validator_count);
}

// Auto-scroll to head node
// Auto-scroll to head node if head tracking is enabled.
const headNode = layout.nodes.find(n => n.root === data.head);
if (headNode) {
const containerRect = container.getBoundingClientRect();
const headY = headNode.y;
const scrollTarget = headY - containerRect.height / 2;
container.scrollTo({
top: Math.max(0, scrollTarget),
behavior: "smooth"
});
const maxScrollTop = Math.max(0, layout.height - containerRect.height);
currentHeadScrollTarget = Math.min(
maxScrollTop,
Math.max(0, headY - containerRect.height / 2)
);

if (followHead) {
scrollToHead();
}
} else {
currentHeadScrollTarget = null;
}
}

Expand All @@ -547,9 +626,37 @@
}

initSVG();
updateFollowHeadToggle();
fetchAndRender();
setInterval(fetchAndRender, POLL_INTERVAL);

followHeadToggle.addEventListener("click", () => {
setFollowHead(!followHead, true);
});

container.addEventListener("scroll", () => {
const scrollingUp = container.scrollTop < lastScrollTop - 2;

if (!isProgrammaticScroll) {
if (followHead && scrollingUp && !isAtHead()) {
setFollowHead(false);
} else if (!followHead && isAtHead()) {
setFollowHead(true);
}
}

lastScrollTop = container.scrollTop;
});

container.addEventListener("wheel", markUserScrollIntent, { passive: true });
container.addEventListener("pointerdown", markUserScrollIntent, { passive: true });
container.addEventListener("touchstart", markUserScrollIntent, { passive: true });
window.addEventListener("keydown", event => {
if (["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " "].includes(event.key)) {
markUserScrollIntent();
}
});
Comment thread
dicethedev marked this conversation as resolved.

window.addEventListener("resize", () => {
if (currentData) render(currentData);
});
Expand Down