From 45b3c3b61cde74e27768ee5a2b5105c00744e6f2 Mon Sep 17 00:00:00 2001 From: Barnabas Busa Date: Wed, 26 Aug 2026 15:59:40 +0200 Subject: [PATCH] Link the builder payload icon to the builder page and show builder names The hard-hat build-source icon next to the proposer linked out to the buildoor's external API URL whenever the inventory knew it, and its tooltip only ever printed the raw builder index. It now always links to the internal /builder/ page and names the builder in the tooltip when known. The dedicated Builder column also dropped the builder name whenever an external URL was known, collapsing to the bare index. It now keeps the name and only surfaces the URL on hover. The local devnet script now detects per-participant buildoor instances (buildoor---) so their names resolve in dora. --- .hack/devnet/run.sh | 27 +++++++++------ handlers/index.go | 14 ++++---- static/js/page-index.js | 12 +++---- templates/blocks/blocks.html | 2 +- templates/index/recentBlocks.html | 4 +-- templates/index/recentSlots.html | 4 +-- templates/slots/slots.html | 2 +- templates/slots_filtered/slots_filtered.html | 2 +- types/models/indexPage.go | 2 ++ utils/format.go | 35 +++++++++++--------- 10 files changed, 57 insertions(+), 47 deletions(-) diff --git a/.hack/devnet/run.sh b/.hack/devnet/run.sh index fe76aae6b..174faf7a2 100755 --- a/.hack/devnet/run.sh +++ b/.hack/devnet/run.sh @@ -42,19 +42,26 @@ fi ## Generate Dora config ENCLAVE_UUID=$(kurtosis enclave inspect "$ENCLAVE_NAME" --full-uuids | grep 'UUID:' | awk '{print $2}') -# Local kurtosis buildoor container, when present, overrides the network-derived URL. -BUILDOOR_CONTAINER=$(docker ps -aq -f "label=kurtosis_enclave_uuid=$ENCLAVE_UUID" \ +# Local kurtosis buildoor containers, when present, override the network-derived URL. +# Matches both the legacy shared "buildoor" service and per-participant instances +# named "buildoor---" (ethereum-package buildoor_params.instances). +BUILDOOR_CONTAINERS=$(docker ps -aq -f "label=kurtosis_enclave_uuid=$ENCLAVE_UUID" \ -f "label=com.kurtosistech.app-id=kurtosis" \ - -f "label=com.kurtosistech.id=buildoor" | head -1) -if [ -n "$BUILDOOR_CONTAINER" ]; then - BUILDOOR_PORT=$(docker inspect --format='{{ (index (index .NetworkSettings.Ports "8080/tcp") 0).HostPort }}' "$BUILDOOR_CONTAINER" 2>/dev/null) - # kurtosis service name (e.g. "buildoor") so dora shows it instead of the forwarded IP:port - BUILDOOR_NAME=$(docker inspect --format='{{ index .Config.Labels "kurtosis_service_name" }}' "$BUILDOOR_CONTAINER" 2>/dev/null) + -f "name=buildoor") +BUILDOOR_URLS="" +for container in $BUILDOOR_CONTAINERS; do + BUILDOOR_PORT=$(docker inspect --format='{{ (index (index .NetworkSettings.Ports "8080/tcp") 0).HostPort }}' "$container" 2>/dev/null) + if [ -z "$BUILDOOR_PORT" ]; then + continue + fi + # kurtosis service name (e.g. "buildoor-lighthouse-geth-1") so dora shows it instead of the forwarded IP:port + BUILDOOR_NAME=$(docker inspect --format='{{ index .Config.Labels "com.kurtosistech.id" }}' "$container" 2>/dev/null) if [ -z "$BUILDOOR_NAME" ]; then BUILDOOR_NAME="buildoor"; fi - if [ -n "$BUILDOOR_PORT" ]; then - BUILDOOR_CONFIG="buildoorUrls: + BUILDOOR_URLS="${BUILDOOR_URLS} - \"${BUILDOOR_NAME}|http://127.0.0.1:${BUILDOOR_PORT}\"" - fi +done +if [ -n "$BUILDOOR_URLS" ]; then + BUILDOOR_CONFIG="buildoorUrls:${BUILDOOR_URLS}" fi BEACON_NODES=$(docker ps -aq -f "label=kurtosis_enclave_uuid=$ENCLAVE_UUID" \ diff --git a/handlers/index.go b/handlers/index.go index 885260b07..ce97e4685 100644 --- a/handlers/index.go +++ b/handlers/index.go @@ -399,15 +399,17 @@ func buildIndexPageRecentEpochsData(ctx context.Context, pageData *models.IndexP // resolveBuildSource maps a db builder index (-1 = self-built) to the model fields // driving the proposer build-source icon (house / hard-hat), matching the slots page. -func resolveBuildSource(dbBuilderIndex int64) (hasBuilder bool, builderIndex uint64, builderURL string) { +func resolveBuildSource(dbBuilderIndex int64) (hasBuilder bool, builderIndex uint64, builderName string, builderURL string) { if dbBuilderIndex == -1 { - return true, math.MaxUint64, "" + return true, math.MaxUint64, "", "" } if dbBuilderIndex < 0 { - return false, 0, "" + return false, 0, "", "" } builderIndex = uint64(dbBuilderIndex) - return true, builderIndex, services.GlobalBeaconService.GetBuilderURL(builderIndex) + builderName = services.GlobalBeaconService.GetValidatorName(builderIndex | services.BuilderIndexFlag) + builderURL = services.GlobalBeaconService.GetBuilderURL(builderIndex) + return true, builderIndex, builderName, builderURL } func buildIndexPageRecentBlocksData(ctx context.Context, pageData *models.IndexPageData, recentBlockCount int) { @@ -447,7 +449,7 @@ func buildIndexPageRecentBlocksData(ctx context.Context, pageData *models.IndexP PayloadStatus: uint8(payloadStatus), BlockRoot: blockData.Root, } - blockModel.HasBuilder, blockModel.BuilderIndex, blockModel.BuilderURL = resolveBuildSource(blockData.BuilderIndex) + blockModel.HasBuilder, blockModel.BuilderIndex, blockModel.BuilderName, blockModel.BuilderURL = resolveBuildSource(blockData.BuilderIndex) if blockData.EthBlockNumber != nil { blockModel.WithEthBlock = true blockModel.EthBlock = *blockData.EthBlockNumber @@ -507,7 +509,7 @@ func buildIndexPageRecentSlotsData(ctx context.Context, pageData *models.IndexPa ForkGraph: make([]*models.IndexPageDataForkGraph, 0), } if dbSlot.Status > 0 { - slotData.HasBuilder, slotData.BuilderIndex, slotData.BuilderURL = resolveBuildSource(dbSlot.BuilderIndex) + slotData.HasBuilder, slotData.BuilderIndex, slotData.BuilderName, slotData.BuilderURL = resolveBuildSource(dbSlot.BuilderIndex) } pageData.RecentSlots = append(pageData.RecentSlots, slotData) blockCount++ diff --git a/static/js/page-index.js b/static/js/page-index.js index 31ed78da4..70dc5f065 100644 --- a/static/js/page-index.js +++ b/static/js/page-index.js @@ -225,7 +225,7 @@ // mirrors utils.FormatProposerWithBuildSource: house = self-built payload, // hard-hat (linking to the builder) = builder-built payload - function formatProposerWithBuildSource(status, idx, name, hasBuilder, builderIdx, builderUrl) { + function formatProposerWithBuildSource(status, idx, name, hasBuilder, builderIdx, builderName) { if(status == 0 || idx >= 9223372036854775807n) { if(idx >= 9223372036854775807n) { return `unknown`; @@ -246,13 +246,9 @@ if(builderIdx >= 18446744073709551615n) { iconHtml = ``; } else { - var builderLink = "/builder/" + builderIdx; - var external = ""; - if(builderUrl) { - builderLink = escapeHtml(builderUrl); - external = ` target="_blank" rel="noopener noreferrer"`; - } - iconHtml = ``; + // the icon links to the builder details page; the tooltip names the builder when known + var builderLabel = builderName ? escapeHtml(builderName) + " (" + builderIdx + ")" : "builder " + builderIdx; + iconHtml = ``; } if(name != "") { diff --git a/templates/blocks/blocks.html b/templates/blocks/blocks.html index 54e319446..b5314f707 100644 --- a/templates/blocks/blocks.html +++ b/templates/blocks/blocks.html @@ -156,7 +156,7 @@

Blocks

{{ end }} {{ if $g.DisplayTime }}{{ formatRecentTimeShort $slot.Ts }}{{ end }} {{ if $slot.Synchronized }} - {{ if $g.DisplayProposer }}{{ if gt $slot.Slot 0 }}{{ formatProposerWithBuildSource $slot.Status $slot.Proposer $slot.ProposerName $slot.HasBuilder $slot.BuilderIndex $slot.BuilderURL }}{{ end }}{{ end }} + {{ if $g.DisplayProposer }}{{ if gt $slot.Slot 0 }}{{ formatProposerWithBuildSource $slot.Status $slot.Proposer $slot.ProposerName $slot.HasBuilder $slot.BuilderIndex $slot.BuilderName }}{{ end }}{{ end }} {{ if $g.DisplayAttestations }}{{ if not (eq $slot.Status 0) }}{{ $slot.AttestationCount }}{{ end }}{{ end }} {{ if $g.DisplayDeposits }}{{ if not (eq $slot.Status 0) }}{{ $slot.DepositCount }} / {{ $slot.ExitCount }}{{ end }}{{ end }} {{ if $g.DisplaySlashings }}{{ if not (eq $slot.Status 0) }}{{ $slot.ProposerSlashingCount }} / {{ $slot.AttesterSlashingCount }}{{ end }}{{ end }} diff --git a/templates/index/recentBlocks.html b/templates/index/recentBlocks.html index 54ee688dc..3a6c1b494 100644 --- a/templates/index/recentBlocks.html +++ b/templates/index/recentBlocks.html @@ -48,7 +48,7 @@
- + {{ html "" }} {{ html "" }} @@ -82,7 +82,7 @@
{{ formatRecentTimeShort $block.Ts }} - {{ formatProposerWithBuildSource $block.Status $block.Proposer $block.ProposerName $block.HasBuilder $block.BuilderIndex $block.BuilderURL }} + {{ formatProposerWithBuildSource $block.Status $block.Proposer $block.ProposerName $block.HasBuilder $block.BuilderIndex $block.BuilderName }} {{ end }} {{ else }} diff --git a/templates/index/recentSlots.html b/templates/index/recentSlots.html index 70b9edf8a..a0c379e43 100644 --- a/templates/index/recentSlots.html +++ b/templates/index/recentSlots.html @@ -52,7 +52,7 @@
0"> - + @@ -109,7 +109,7 @@
{{ formatRecentTimeShort $slot.Ts }} - {{ if gt $slot.Slot 0 }}{{ formatProposerWithBuildSource $slot.Status $slot.Proposer $slot.ProposerName $slot.HasBuilder $slot.BuilderIndex $slot.BuilderURL }}{{ end }} + {{ if gt $slot.Slot 0 }}{{ formatProposerWithBuildSource $slot.Status $slot.Proposer $slot.ProposerName $slot.HasBuilder $slot.BuilderIndex $slot.BuilderName }}{{ end }} {{ end }} {{ else }} diff --git a/templates/slots/slots.html b/templates/slots/slots.html index c842db3bf..c5c658c4d 100644 --- a/templates/slots/slots.html +++ b/templates/slots/slots.html @@ -155,7 +155,7 @@

Slots

{{ end }} {{ if $g.DisplayTime }}{{ formatRecentTimeShort $slot.Ts }}{{ end }} {{ if $slot.Synchronized }} - {{ if $g.DisplayProposer }}{{ if gt $slot.Slot 0 }}{{ formatProposerWithBuildSource $slot.Status $slot.Proposer $slot.ProposerName $slot.HasBuilder $slot.BuilderIndex $slot.BuilderURL }}{{ end }}{{ end }} + {{ if $g.DisplayProposer }}{{ if gt $slot.Slot 0 }}{{ formatProposerWithBuildSource $slot.Status $slot.Proposer $slot.ProposerName $slot.HasBuilder $slot.BuilderIndex $slot.BuilderName }}{{ end }}{{ end }} {{ if $g.DisplayAttestations }}{{ if not (eq $slot.Status 0) }}{{ $slot.AttestationCount }}{{ end }}{{ end }} {{ if $g.DisplayDeposits }}{{ if not (eq $slot.Status 0) }}{{ $slot.DepositCount }} / {{ $slot.ExitCount }}{{ end }}{{ end }} {{ if $g.DisplaySlashings }}{{ if not (eq $slot.Status 0) }}{{ $slot.ProposerSlashingCount }} / {{ $slot.AttesterSlashingCount }}{{ end }}{{ end }} diff --git a/templates/slots_filtered/slots_filtered.html b/templates/slots_filtered/slots_filtered.html index 153849680..85e6587ba 100644 --- a/templates/slots_filtered/slots_filtered.html +++ b/templates/slots_filtered/slots_filtered.html @@ -358,7 +358,7 @@

Filtered Slots

{{ formatRecentTimeShort $slot.Ts }} {{- end }} {{- if $g.DisplayProposer }} - {{ if gt $slot.Slot 0 }}{{ formatProposerWithBuildSource $slot.Status $slot.Proposer $slot.ProposerName $slot.HasBuilder $slot.BuilderIndex $slot.BuilderURL }}{{ end }} + {{ if gt $slot.Slot 0 }}{{ formatProposerWithBuildSource $slot.Status $slot.Proposer $slot.ProposerName $slot.HasBuilder $slot.BuilderIndex $slot.BuilderName }}{{ end }} {{- end }} {{- if $g.DisplayAttestations }} {{ if not (eq $slot.Status 0) }}{{ $slot.AttestationCount }}{{ end }} diff --git a/types/models/indexPage.go b/types/models/indexPage.go index aa715d852..43a156a97 100644 --- a/types/models/indexPage.go +++ b/types/models/indexPage.go @@ -85,6 +85,7 @@ type IndexPageDataBlocks struct { PayloadStatus uint8 `json:"payload_status"` HasBuilder bool `json:"has_builder"` BuilderIndex uint64 `json:"builder_index"` + BuilderName string `json:"builder_name"` BuilderURL string `json:"builder_url"` BlockRoot []byte `json:"block_root" ssz-size:"32"` } @@ -100,6 +101,7 @@ type IndexPageDataSlots struct { PayloadStatus uint8 `json:"payload_status"` HasBuilder bool `json:"has_builder"` BuilderIndex uint64 `json:"builder_index"` + BuilderName string `json:"builder_name"` BuilderURL string `json:"builder_url"` Safe bool `json:"safe"` BlockRoot []byte `json:"block_root" ssz-size:"32"` diff --git a/utils/format.go b/utils/format.go index 88a110185..2ca66d101 100644 --- a/utils/format.go +++ b/utils/format.go @@ -894,7 +894,7 @@ func formatValidator(index uint64, name string, icon string, withIndex bool) tem // // Scheduled/missing slots (status == 0) and unknown proposers have no // determinable build source and are rendered without any leading icon. -func FormatProposerWithBuildSource(status uint8, index uint64, name string, hasBuilder bool, builderIndex uint64, builderURL string) template.HTML { +func FormatProposerWithBuildSource(status uint8, index uint64, name string, hasBuilder bool, builderIndex uint64, builderName string) template.HTML { if status == 0 || index == math.MaxInt64 { if index == math.MaxInt64 { return template.HTML(`unknown`) @@ -914,15 +914,9 @@ func FormatProposerWithBuildSource(status uint8, index uint64, name string, hasB // self-built payload iconHTML = `` } else { - // builder-built payload - link the icon to the builder URL when known, - // otherwise to the internal builder page - builderLink := fmt.Sprintf("/builder/%v", builderIndex) - external := "" - if builderURL != "" { - builderLink = html.EscapeString(builderURL) - external = ` target="_blank" rel="noopener noreferrer"` - } - iconHTML = fmt.Sprintf(``, builderLink, external, builderIndex) + // builder-built payload - the icon links to the builder details page and + // names the builder in the tooltip when the buildoor inventory knows it + iconHTML = fmt.Sprintf(``, builderIndex, html.EscapeString(builderTooltipLabel(builderIndex, builderName))) } nameLabel := fmt.Sprintf("%v", index) @@ -934,6 +928,15 @@ func FormatProposerWithBuildSource(status uint8, index uint64, name string, hasB return template.HTML(fmt.Sprintf(`%v %v`, labelClass, iconHTML, index, nameLabel)) } +// builderTooltipLabel renders "name (index)" when the builder has a known name and +// "builder " otherwise. +func builderTooltipLabel(index uint64, name string) string { + if name != "" { + return fmt.Sprintf("%v (%v)", name, index) + } + return fmt.Sprintf("builder %v", index) +} + func FormatValidatorNameWithIndex(index uint64, name string) template.HTML { if name != "" { return template.HTML(fmt.Sprintf("%v (%v)", html.EscapeString(name), index)) @@ -970,11 +973,11 @@ func formatBuilder(index uint64, name string, externalURL string, icon string, w return template.HTML(" Self-built") } - // When the builder exposes an external URL, its "name" is often the full API URL, which is - // far too long for the table columns. Collapse it to the hyperlinked builder index and show - // the full API URL on hover instead of printing it inline. + // The label always links to the builder details page. When the buildoor inventory knows the + // instance's API URL it is shown on hover only, never printed inline or linked to. + tooltip := "" if externalURL != "" { - return template.HTML(fmt.Sprintf(" %v", icon, index, html.EscapeString(externalURL), index)) + tooltip = fmt.Sprintf(" data-bs-toggle=\"tooltip\" data-bs-placement=\"top\" data-bs-title=\"%v\"", html.EscapeString(externalURL)) } if name != "" { @@ -984,9 +987,9 @@ func formatBuilder(index uint64, name string, externalURL string, icon string, w } else { nameLabel = html.EscapeString(name) } - return template.HTML(fmt.Sprintf(" %v", icon, index, nameLabel)) + return template.HTML(fmt.Sprintf(" %v", icon, index, tooltip, nameLabel)) } - return template.HTML(fmt.Sprintf(" %v", icon, index, index)) + return template.HTML(fmt.Sprintf(" %v", icon, index, tooltip, index)) } func FormatRecentTimeShort(ts time.Time) template.HTML {