indexer v1.4.6 -> v1.4.7 and pass the chain's RPC to the standalone API, so
native-coin balances are read from the node rather than from
evm_addresses.balance — a column the indexer writes as the literal '0' on
first sight and never updates. Every address page reported a balance of
zero, WLUX's 159,126,795.518 LUX included. v1.4.7 also counts token holders
and transfers from the rows that hold them, stops calling the base fee
"burnt" on a chain that credits the whole fee to the coinbase, and returns
addresses on the evm_* schema instead of an empty page.
The network switcher advertised testnet.explore.lux.network and
devnet.explore.lux.network. Neither resolves; the live hosts are
explorer.lux-{test,dev}.network. networksForHost already derives sibling
hostnames from the request host and derives them correctly, so the
switcher now takes its domains from there and config keeps only what
config knows — the label and the chain ID. Devnet's was wrong too:
96370, where the devnet genesis cChainGenesis.config.chainId and a live
eth_chainId both say 96367.
Co-authored-by: Hanzo Dev <[email protected]>
The Go binary is now a data-layer service only. The `/` catch-all that
served an embedded SPA shell is gone; v1.3.0 keeps just the endpoints
the FE depends on:
- /v1/* — indexer + blockscout-compat REST APIs
- /health — health probe
- /envs.js — runtime brand/chain config (window.ENV)
- /icon.svg, /logo.svg — per-host brand SVGs (overlay-friendly)
- /icon-{slug}.svg, /logo-{slug}.svg — extra named brand assets
Every other path returns 404 so the IngressRoute hands it off to the
per-brand Blockscout FE Deployment (ghcr.io/{brand-org}/explore-{env}).
Concrete deltas:
- frontend.go: drop handleSPA, writeIndex, f.index; replace with
handleNamedBrandAsset + brandAssetNames discovery
- frontend_test.go: TestFrontendNoSPA + TestFrontendBrandAssetsServed
replace the index-html overlay tests
- Dockerfile: drop Next.js frontend build stage; runtime image is just
the Go binary + brand SVGs (no embedded HTML)
- static/index.html: removed (60-byte placeholder)
The per-brand FE Deployments + IngressRoute patches that complete this
split live in the cluster (lux-mainnet + lux-testnet namespaces). Source
of the FE images: luxfi/explore via Dockerfile.branded.
handleEnvs now returns VITE_CURRENT_NETWORK + VITE_NETWORK_HOSTS so
the SPA can render a Network dropdown next to the Chain picker. When
the user picks "testnet" from explore.zoo.network the SPA redirects
to explorer.zoo-test.network (and vice-versa).
Sibling host derivation lives entirely in Go:
- parseBrandEnv extracts (brand, env, dotted, ok) from any of our
deployed host shapes — explore.{brand}.tld, explorer.{brand}-test.network,
explore-{brand}.lux.network, explore-{brand}-test.lux.network.
- networksForHost composes the sibling map. Mainnet TLD is preserved
when the user is on a brand-owned host (explore.hanzo.ai stays .ai);
testnet is always {brand}-test.network because that's the canonical
shape.
- isValidBrand rejects numeric stems so an IP host (10.0.0.1) doesn't
fall through to "explore.10.0.0.1" / "explorer.10-test.network".
SPA renders the dropdown above the chain picker; hidden when the host
yields fewer than two siblings (local dev, admin tooling).
Tests cover all four canonical surfaces (lux+zoo × mainnet+testnet),
hanzo's TLD asymmetry (.ai mainnet → .network testnet), the
explore-{slug}.lux.network prefix shape, and the unparseable-host
guards (IPs, in-cluster service names, localhost).
- host matcher now accepts explorer.* (alias of explore.*) and
{prefix}.{slug}-{env}.tld shapes — so explorer.zoo-test.network
resolves to the Zoo brand (was falling through to Lux default).
- new chainSlugForHost helper drives a default-chain promotion in
handleEnvs: when the host pins a chain, the SPA opens on that
chain instead of the config-default cchain. explore.zoo.network
now defaults to Zoo, not C-Chain.
- factored host matching into pure hostMatchesSlug predicate so
brand and slug paths share one ground truth.
- tests cover both regressions (explorer.zoo-test.network → Zoo,
explore.zoo.network → slug=zoo) plus the existing
cross-contamination guards (zoofoo must not match zoo).
brandStruct only matched bare "{slug}" and "{slug}.tld" host shapes —
but the unified explorer ships behind IngressRoutes whose Host()
patterns are exclusively `explore-{slug}.lux.network` and
`api-explore-{slug}.lux.network` plus brand-owned roots like
`explore.hanzo.ai`, `explore.zoo.ngo`, `explore.pars.network`. With
the old matcher every per-chain host fell through to BrandDefault
("Lux Explorer"), so per-chain theming silently no-op'd in prod
(verified 2026-06-05: explore-hanzo.lux.network /envs.js returned
"VITE_BRAND":{"name":"Lux Explorer"} instead of Hanzo).
New matcher recognises the four prefix shapes the IngressRoutes
produce, both with and without an env suffix (-test / -dev):
- explore-{slug}.tld... UI host on lux.network
- api-explore-{slug}.tld... API host on lux.network
- explore-{slug}-{env}.tld... UI host for testnet / devnet variants
- api-explore-{slug}-{env}... API host for testnet / devnet variants
- explore.{slug}.tld... UI on brand-owned domain
- api-explore.{slug}.tld... API on brand-owned domain
Plus the prior bare-{slug} and {slug}-prefix shapes for backward
compat.
Test coverage: TestBrandStructHostMatching, 18 cases incl. the
explore-spcfoo cross-contamination guard ensuring partial-slug-
suffix matches don't bleed.
Two related changes that get the explorer SPA rendering real chain
data without waiting for upstream luxfi/explore to ship a Next.js
static export.
1. Runtime SPA overlay (config.go, frontend.go)
New cfg.StaticDir (also EXPLORER_STATIC_DIR env var). When set,
the frontend prefers the on-disk copy of any static asset over the
embedded FS — so a ConfigMap-mounted SPA shell replaces the
embedded 60-byte placeholder without rebuilding the binary. The
embedded FS remains the source of truth at build time and the
fallback when no overlay is configured.
handleSPA + serveBrandFile both check overlay > embedded. Tests
cover overlay-present, overlay-missing-file fallback, no-overlay,
and env-var-only configuration.
2. Substantive SPA shell (static/index.html)
Replaced the 60-byte stub (<div id="root"></div>) with a vanilla
JS SPA — ~7KB, zero dependencies, no React/Next.js. Reads
window.ENV from /envs.js, fetches /v1/indexer/{slug}/{stats,blocks,
transactions}, renders KPIs + Latest Blocks + Latest Transactions
with per-chain switching and brand color/title overrides. Polls
every 15s. Stores selected chain in URL hash + localStorage.
This is the explorer's default UI until upstream luxfi/explore
ships output: 'export' and the Dockerfile's optional pnpm-build
overlay activates.
Dockerfile: drop the leftover merge-conflict markers around the SPA
build stage, keep the explanatory comment.
Tests: 4 new in frontend_test.go covering overlay precedence + env-var
fallback. All existing tests (registry, realtime) still pass.