The dex subgraph answered `{"data":{"markets":null}}` on a 200 while being
mounted, subscribed to the 0x9999 DEXFill topic and caught up to the chain
head. A GraphQL list field answering null reads as "not deployed / erroring",
so a DEX that had simply seen no fills was indistinguishable from a dead pipe.
luxfi/graph v1.2.13 makes an empty result an empty list.
CI moves to .hanzo/workflows/release.yml on the native git.hanzo.ai runners,
matching luxfi/graph. .github/workflows/docker.yml ran on `runs-on: lux-build`,
an ARC pool that no longer exists — the clusters have no ARC CRDs and
github.com/luxfi reports zero runners at org and repo scope, so every v* tag
queued a job nothing could pick up and waited instead of failing. That is why
v1.5.4 is a tag with no image. One CI path, one that builds.
LLM.md/CLAUDE.md become local-only, per the org convention.
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.
- BRAND is now a merge of SERVER_BRAND (chains.yaml-supplied) and
INFERRED_BRAND (host parser table). Server wins on name/coin/accent
when it's non-default; inferred fills in iconUrl/logoUrl when the
chain config doesn't carry per-brand SVG paths. Stops Zoo rendering
with the default Lux favicon when chains.yaml omits icon_url.
- networkFromHost now canonicalises "test"→"testnet", "dev"→"devnet"
so the dropdown label matches the server-side VITE_CURRENT_NETWORK.
The Go-side networksForHost was added in v1.2.20 but the deployed
image is still v1.2.18 (CI pool gap — lux-build AutoscalingRunnerSet
is missing from arc-system). The SPA now also parses location.host
client-side so the network switcher works when /envs.js doesn't ship
VITE_CURRENT_NETWORK / VITE_NETWORK_HOSTS.
Same shape rules as the Go side: explore.{brand}.{tld} (mainnet,
preserve tld), explorer.{brand}-test.network (testnet, .network is
canonical). Numeric and *.local hosts skip the dropdown.
This lets us ship the cross-network switcher via an EXPLORER_STATIC_DIR
overlay (ConfigMap-mounted SPA) without rebuilding the image.
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).
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.
Module renamed to github.com/luxfi/explorer (was -unified). Imports
luxfi/indexer/{daemon,evm,storage,explorer} and luxfi/graph/{engine,
indexer,storage} as libraries; embeds the luxfi/explore SPA via go:embed
all:static.
A new ChainSupervisor wraps the existing ChainRegistry: adding a chain
(via config / mDNS / admin API) spawns a per-chain indexer goroutine plus
one graph engine + indexer per enabled subgraph; removing cancels them
and frees the routes from a sync.Map dispatcher mounted on /v1/indexer/,
/v1/explorer/ (legacy alias), and /v1/graph/.
Config schema is fully per-chain: indexer poll interval, list of graph
subgraphs (each can pick a built-in schema like amm, securities, fhe…),
brand (name/coin/accent + on-disk icon/logo paths), token features, and
top-level networks list. /envs.js exposes the chain list and active
brand to the SPA at runtime, so a deploy can rebrand by remounting
chains.yaml + assets — no image rebuild.
Dockerfile clones luxfi/explore + luxfi/indexer + luxfi/graph as
siblings inside the build, populates ./static from the SPA build, and
produces one binary. CI checks out the three repos as siblings so the
local replace in go.mod works without tagged releases.