The live stream answers under the page's API base, and block time reads in seconds

The page builds its socket URL from its API base, so a brand page whose base is
/v1/indexer/<slug> opened /v1/indexer/<slug>/v1/base/realtime and got a 404.
The stream now answers there, scoped to <slug>.

indexer v1.5.10 reports average_block_time in milliseconds, which the page
divides by 1000. The image builds on Go 1.27, the version go.mod requires.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-09-22 23:41:28 -07:00
co-authored by Hanzo Dev
parent a13033fc40
commit b0fed15c5f
6 changed files with 25 additions and 16 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
# per-brand Blockscout FE Deployment (ghcr.io/{brand-org}/explore-{env}).
# Built from luxfi/explore via Dockerfile.branded — see ~/work/lux/explore.
ARG GO_VERSION=1.26
ARG GO_VERSION=1.27
# ---- Stage 1: build the unified FE + indexer + graph binary ----
# graph + indexer are consumed as Go modules pinned in go.mod (luxfi/graph
+1 -1
View File
@@ -6,7 +6,7 @@ require (
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
github.com/hanzoai/csqlite v0.1.2
github.com/luxfi/graph v1.2.56
github.com/luxfi/indexer v1.5.8
github.com/luxfi/indexer v1.5.10
github.com/luxfi/mdns v0.1.1
gopkg.in/yaml.v3 v3.0.1
)
+2 -4
View File
@@ -181,14 +181,12 @@ github.com/luxfi/database v1.21.1 h1:GNnoWVa82l+n2dK7x2aG8LR2NEToq6ZCRX0sQjmK0OM
github.com/luxfi/database v1.21.1/go.mod h1:Gc7Z2OPrrcYLnAL8B1trOnguXauOlSDV5tkviLN6Xec=
github.com/luxfi/geth v1.20.1 h1:QUGQr4AKvADjwMi7t8a0OfoyxShgEcI9pwie1jFYfm0=
github.com/luxfi/geth v1.20.1/go.mod h1:GV5bIMEgWviRN+jPXERyVpI16H3iHqPcdIokDoZdrvU=
github.com/luxfi/graph v1.2.53 h1:v4Wfu9793qsCAsev4H6T9x336AkQ/hXboHqtINN/YB8=
github.com/luxfi/graph v1.2.53/go.mod h1:hMvCipVl2DqbaqlZGNSdENr4el9Z6OL0PidzgpRA9gs=
github.com/luxfi/graph v1.2.56 h1:KXwKL3MV2Jda0ScZJxnyV+WR5sOiiobXzKF/B3IUcno=
github.com/luxfi/graph v1.2.56/go.mod h1:ogDT+6hMj61kRjp1/euMMZdU1xzFS5k8/Ci3vrkbYxI=
github.com/luxfi/ids v1.3.2 h1:c6Rft5kZB4XqiCtWaGH47bfhaNFm3FGRfhEzI01GVeI=
github.com/luxfi/ids v1.3.2/go.mod h1:+5l8cYMbKpORJbQ2r98CYJo9TQATgUdnmzpYFZWMwwc=
github.com/luxfi/indexer v1.5.8 h1:m//EZyUoo8Of5M7DMGseOTOoIOkXZ9X/4hTi34YGZGE=
github.com/luxfi/indexer v1.5.8/go.mod h1:oypCgtib03LylLJY5TQYct8CAhm16gew6d83PusPFoc=
github.com/luxfi/indexer v1.5.10 h1:6J0CcuZ0PuZVZG53EqB3/IGRK4hdL/dVoLSdUkH3pVw=
github.com/luxfi/indexer v1.5.10/go.mod h1:eZS/x4Ed1e2rDqsaj8yMl3az8tB9t+m7jvgC+a1SYVs=
github.com/luxfi/log v1.4.3 h1:xkUKRWvQ4ZwvlUC2e0/RTtHYZOYSMvSQ9W9lbjwBmiI=
github.com/luxfi/log v1.4.3/go.mod h1:myIkufyiQomSQH34K981kbz6cG4WUoerRUh7F4XhlQI=
github.com/luxfi/math v1.5.1 h1:FDOY75e4vn/Xra1ij99xOS/9XdxQGCPP6HONHRkCwfg=
+4
View File
@@ -168,6 +168,10 @@ func main() {
realtime := registry.hub.HandleMultiplexedSSE(cfg)
mux.HandleFunc("GET /v1/base/realtime", realtime)
mux.HandleFunc("HEAD /v1/base/realtime", realtime)
// A page whose API base is /v1/indexer/<slug> builds its socket URL from
// that base, so the same stream answers under the prefix, scoped to <slug>.
mux.HandleFunc("GET /v1/indexer/{chain}/v1/base/realtime", realtime)
mux.HandleFunc("HEAD /v1/indexer/{chain}/v1/base/realtime", realtime)
supervisor.MountRoutes(mux)
supervisor.MountSwap(mux)
+8 -4
View File
@@ -84,11 +84,15 @@ func (r *sseRegistry) fanout(chain string, envelope []byte) {
}
}
// streamChain is the chain an SSE subscriber receives: whatever `?chain=`
// asks for, else the chain the request host is served. Never empty, because
// an empty filter means "every chain the hub broadcasts" and no brand host
// wants another brand's blocks.
// streamChain is the chain an SSE subscriber receives: the chain its path
// names (/v1/indexer/{chain}/v1/base/realtime), else whatever `?chain=` asks
// for, else the chain the request host is served. Never empty, because an
// empty filter means "every chain the hub broadcasts" and no brand host wants
// another brand's blocks.
func streamChain(cfg Config, r *http.Request) string {
if p := r.PathValue("chain"); p != "" {
return p
}
if q := strings.TrimSpace(r.URL.Query().Get("chain")); q != "" {
return q
}
+9 -6
View File
@@ -200,8 +200,8 @@ func TestRealtimeStatsEndpoint(t *testing.T) {
}
}
// The realtime stream carries the chain of the host it was opened on, and
// nothing else. One binary indexes five chains and broadcasts all of them onto
// The realtime stream carries the chain its path or host names, and nothing
// else. One binary indexes five chains and broadcasts all of them onto
// one hub; an unscoped stream put Lux and Zoo blocks into Hanzo's block list,
// which is how a 50-block page rendered 63 rows.
func TestRealtimeStreamCarriesOnlyTheHostsChain(t *testing.T) {
@@ -214,18 +214,21 @@ func TestRealtimeStreamCarriesOnlyTheHostsChain(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/base/realtime", hub.HandleMultiplexedSSE(cfg))
mux.HandleFunc("GET /v1/indexer/{chain}/v1/base/realtime", hub.HandleMultiplexedSSE(cfg))
ts := httptest.NewServer(mux)
defer ts.Close()
for _, tc := range []struct {
host string
path string
want string
}{
{"api-explore.hanzo.network", "hanzo"},
{"explore.zoo.network", "zoo"},
{"api-explore.lux.network", "cchain"}, // names no chain -> the default
{"api-explore.hanzo.network", "/v1/base/realtime", "hanzo"},
{"explore.zoo.network", "/v1/base/realtime", "zoo"},
{"api-explore.lux.network", "/v1/base/realtime", "cchain"}, // names no chain -> the default
{"api-explore.lux.network", "/v1/indexer/zoo/v1/base/realtime", "zoo"}, // the path outranks the host
} {
req, err := http.NewRequest(http.MethodGet, ts.URL+"/v1/base/realtime", nil)
req, err := http.NewRequest(http.MethodGet, ts.URL+tc.path, nil)
if err != nil {
t.Fatal(err)
}