Zoo's home rendered no P-Chain panel but still sent the validators, chain list and height reads behind the Lux panels, because the hooks ran in the page body. The metrics strip and the chain-health grid are now components the Lux explorer alone renders, and the reads live inside them. The stats page's chain count, shown only on Lux, reads the chain list the same way. NetworkOverview.spec.tsx asserts a brand host sends no P-Chain request and the Lux host sends all three; putting the reads back in the page body fails it. Claude-Session: https://claude.ai/code/session_011Ge9UnfkEK37Lapx5nfPQK
54 KiB
Lux Network Explorer - AI Knowledge Base
Last Updated: 2024-12-24
Project: explore
Organization: luxfi
Upstream: blockscout/frontend
Project Overview
Lux Network Explorer is a fork of Blockscout Frontend, customized for the Lux blockchain ecosystem. It provides a web-based interface for exploring blocks, transactions, addresses, tokens, and smart contracts on Lux Network chains (C-Chain, Zoo chains).
Essential Commands
The package manager is pnpm (packageManager: [email protected], only pnpm-lock.yaml
exists). yarn in older notes below is wrong — yarn install would write a
second lockfile.
# Development — THE dev command
pnpm dev # tools/scripts/dev.sh, serves on $NEXT_PUBLIC_APP_PORT (3055), NOT 3000
pnpm build:next # Production build WITH assets/sprite/envs.js
pnpm start # Start production server
# Linting & Testing
pnpm vitest run # Unit suite — the only gate in .hanzo/workflows/ci.yml
pnpm lint:eslint # ESLint (repo-wide is red on main; keep YOUR files clean)
pnpm lint:tsc # TypeScript check (also red on main — see .hanzo/workflows/ci.yml)
pnpm test:pw # Playwright
What pnpm dev actually does
tools/scripts/dev.sh deletes and regenerates the runtime env bundle each start:
rm -rf public/assets/{configs,multichain,envs.js}download_assets.sh— skips every unsetNEXT_PUBLIC_*asset URL, so it is offline-safe with the committed local env filesbuild_sprite.sh— needsjqandmd5sum(not macOS defaults)make_envs_script.sh→public/assets/envs.js— this is the file the browser reads.pages/_document.tsxloads it as a blocking<script>andconfigs/app/utils.ts:getEnvValuereadswindow.__envsfrom it. Plainnext devdoes NOT generate it, and without it every API-backed page throwsAPI config for general not found.next dev -p $NEXT_PUBLIC_APP_PORT
Env precedence is first -e wins (dotenv-cli, override: false):
.env.secrets → .env.development.local → .env.local → .env.development → .env.
Missing files are skipped silently, which is why the absent .env.secrets is harmless.
⚠️ .env.development.local sets NEXT_PUBLIC_APP_HOST=explore.lux.network, so
the local app self-identifies as production for absolute links and OIDC redirects.
🪤 .nvmrc contains >=22.14.0, which is a semver range, not an nvm version —
nvm use fails in this directory. CI pins the real value, 22.14.0.
🪤 CSS comments do not nest. A /* … */ inside a block comment in
nextjs/global.css ends it early, and the remaining prose is parsed as CSS —
Turbopack then serves a full-page Build Error overlay instead of the app, so
pnpm dev "starts" but nothing renders. Fixed once; do not reintroduce.
# Docker
docker compose up # Dev environment
docker compose -f compose.prod.yml up # Production
Architecture
explore/
├── pages/ # Next.js pages (file-based routing)
├── ui/ # UI components by feature
│ ├── address/ # Address page components
│ ├── block/ # Block page components
│ ├── tx/ # Transaction page components
│ ├── token/ # Token page components
│ ├── shared/ # Shared components
│ └── snippets/ # Header, footer, navigation
├── lib/ # Core business logic
│ ├── api/ # API client & services
│ ├── hooks/ # React hooks
│ ├── contexts/ # React contexts
│ └── web3/ # Web3 integration
├── toolkit/ # UI toolkit (Chakra-based)
│ ├── components/ # Reusable components
│ ├── theme/ # Theme configuration
│ └── chakra/ # Chakra UI customization
├── configs/ # Configuration files
│ ├── app/ # App features config
│ └── envs/ # Environment presets
├── deploy/ # Deployment tools
│ ├── scripts/ # Build scripts
│ └── tools/ # Deployment utilities
├── mocks/ # Mock data for testing
├── stubs/ # API type stubs
├── types/ # TypeScript types
└── nextjs/ # Next.js configuration
Key Technologies
| Category | Technology |
|---|---|
| Framework | Next.js 14 (Pages Router) |
| Language | TypeScript |
| UI Library | Chakra UI v3 |
| State | React Query (TanStack) |
| Web3 | wagmi, viem, RainbowKit |
| Testing | Playwright, Vitest |
| Styling | Emotion CSS-in-JS |
| Build | Turbopack |
Configuration
Environment Variables
Key environment variables (see docs/ENVS.md for full list):
NEXT_PUBLIC_NETWORK_NAME=Lux Network
NEXT_PUBLIC_NETWORK_ID=96369
NEXT_PUBLIC_NETWORK_RPC_URL=https://api.lux.network/v1/chain/C/rpc
NEXT_PUBLIC_API_HOST=https://explore.lux.network
NEXT_PUBLIC_APP_HOST=https://explore.lux.network
Lux Branding
Lux-specific branding is configured in:
configs/app/ui/- UI customizationpublic/logos/- Logo assetstoolkit/theme/- Theme colors
Development Workflow
Syncing with Upstream
# Fetch upstream changes
git fetch upstream
# Merge upstream into main
git merge upstream/main
# Resolve conflicts, preserving Lux branding
git add . && git commit
Adding New Features
- Check if feature exists in upstream Blockscout
- If not, implement in
ui/orlib/directories - Add TypeScript types in
types/ - Add mock data in
mocks/for testing - Test with Playwright
Lux-Specific Changes
Key Lux customizations:
ui/snippets/networkLogo/- Lux logo componenttoolkit/theme/foundations/- Lux color palettepublic/logos/- Lux brand assetsDockerfile.lux- Lux-specific Docker build
Chain Configuration
| Chain | Chain ID | Explorer URL |
|---|---|---|
| Lux Mainnet (C-Chain) | 96369 | explore.lux.network |
| Lux Testnet | 96368 | testnet.explore.lux.network |
| Zoo Mainnet | 200200 | zoo.explore.lux.network |
| Zoo Testnet | 200201 | zoo-testnet.explore.lux.network |
API Integration
The explorer connects to:
- Blockscout API: Block/tx/address data
- Stats API: Chain statistics
- RPC Node: Direct chain queries
API services are in lib/api/services/.
Context for AI Assistants
This file (LLM.md) is symlinked as:
AGENTS.mdCLAUDE.mdQWEN.mdGEMINI.md
All symlinks reference this single source of truth.
Rules for AI Assistants
- ALWAYS update LLM.md with significant discoveries
- PRESERVE Lux branding when merging upstream changes
- DO NOT modify Blockscout-specific CI workflows
- USE
pnpmfor package management (repo ispackageManager: [email protected]; npm/yarn break the@hanzoguioverrides) - TEST UI changes with Playwright before committing
Recent Changes
The P-Chain is read through typed GETs, from the P-Chain that secures the chain (v2.8.6-lux)
- luxd serves no P-Chain JSON-RPC;
platform.*404s on every node of this generation. The P-Chain isGET <origin>/v1/chain/P/ops/{validators,blockchains,height}, contract atops/.well-known/openapi.json(OpenAPI 3.1). The browser reads it throughGET /v1/node/p-chain?op=<op>(pages/api/node/[endpoint].ts), whose fixed table allows those three ops and one parameter (nodeIDs, a single base58 NodeID). JSON-RPC POSTs there answer 405. - Delegator records come only from a read naming ONE validator
(
service.go: numNodeIDs == 1); the list carriesdelegatorCountanddelegatorWeight.useCurrentValidatorsreads each validator with delegators on its own, and no others. ChainEntry.pChainis WHERE the P-Chain that secures a chain is read ({ url, name, symbol }), read bygetPChain(host?). Lux:api.lux.network. Zoo is an L2 carried by Lux's validator set: Lux's too, neverapi.zoo.network(a stopgap node whose stakers are not Zoo's security). Hanzo: absent, since its chain runs its own 5-seat committee until hanzod joins as an L2. White-label hosts: the origin ofNEXT_PUBLIC_NETWORK_RPC_URL.explore.zoo-test.networkis registered so it reads Lux testnet's.- Absent P-Chain: no P-Chain request, no Validators link, no stats panel, and
/validators404s (guards.pChain). Chains, Bridge and DEX are the Lux primary network's own chains: shown and served (guards.primaryNetwork) only on the Lux C-Chain host. The top bar and the mobile menu gate on the same two predicates. - P-Chain UI is labelled by whose it is: "Lux primary network validators securing Zoo Network", stake in LUX. The cross-network validator sum is gone: with Zoo carried by Lux's set it counted the same validators twice.
- A P-Chain at genesis answers height
"0"; that renders as#0, while an unanswered read renders nothing. Lux mainnet's archive has no peers, so its height is 0 with 5 genesis validators whose stake ends 2026-12-12. - A surface reads only what it renders: the home page's P-Chain panels and the
stats page's chain count are components the Lux explorer alone renders, so a
brand explorer's home sends no P-Chain request (v2.8.7-lux,
ui/pages/NetworkOverview.spec.tsx). - Pinned by
pages/api/node/[endpoint].spec.ts,lib/api/pchain/gate.spec.tsx,lib/api/pchain/wire.spec.tsandconfigs/app/chainRegistry.spec.ts.
Duplicated tokens filter, colliding footer row, lower-case addresses (v1.1.27)
ui/tokens/TokensActionBar.tsxwas the one action bar the Chakra→Tailwind port stripped. Upstream has two blocks on purpose —<HStack display={{base: 'flex', lg:'none'}}>(filter + sort + search, mobile) and adisplay={{base: 'none', lg:'flex'}}block inside the stickyActionBar(filter + search, desktop). The port turned both into bare<div>s, so both rendered at every width (two Filter buttons, two search inputs) and, with noflex, their children stacked instead of sitting in a row. Every sibling kept the pattern —VerifiedContracts,NameDomainsActionBar,ClustersActionBarall useflex lg:hidden/hidden lg:flex— so the fix restores it rather than inventing a second way. 🪤 When a page-level control looks doubled, count the elements before blaming CSS: two things at one spot are usually two things. ⚠️NameDomainsActionBarwrites its desktop block asflex hidden lg:block—block, so its filter and input still stack at lg. Same family, not fixed here.- @luxfi/ui's
ButtonFramenever setsflexDirection. It isstyled(View)on a react-nativeView, whose default iscolumn, while the frame setsinline-flex,alignItems,gap, a fixedheightandoverflow: hidden— all of which only make sense for a row. Any Button with two or more children stacks icon over label and the fixed height clips the label off. Swept 39 visible LuxButtons over 9 pages × 2 viewports: only 3 have >1 child and onlyFilterclipped, so this is fixed atui/shared/filters/FilterButton.tsx(the single component behind every filter in the app) with an inlinestyle={{ flexDirection: 'row' }}— inline because the frame's ownis_Viewclass outranks a Tailwindflex-rowutility. The primitive is still wrong; fixButtonFrameupstream and drop the call-site style. Footer's no-custom-links branch never wrapped its left column. The grid islg:grid-cols-[minmax(auto,470px)_1fr]but that branch passesrenderNetworkInfo() / renderProjectInfo() / renderRecaptcha()as three separate children, so auto-placement scattered them and the link row landed in the 470px track — where five 160px columns cannot fit. The two mobilegrid-cols-2tracks were also still the only explicit tracks atlg, so they took1freach, collapsed to width 0, and Contribute / X (Twitter) / Discord painted on top of each other bottom-left. Both halves fixed: wrap the left column in<div className="min-w-0">exactly as the custom-links branch already does, and addlg:grid-cols-none. 🪤 A zero-width element does not register as an overlap in a rect-intersection test — its text still paints over the neighbour. Comparemax(width, scrollWidth), and treat width 0 as a defect.- The API case-folds address hashes, so never trust the payload's casing. The
backend lower-cases
{hash}/{addr}path params before lookup (deliberate — a checksummed URL used to 404).ui/pages/Address.tsxandui/multichain/address/MultichainAddress.tsxboth readaddressQuery.data?.hash ?? getCheckedSummedAddress(hash), and the??took the lower-case payload whenever the address existed. Both now checksum whatever they display;getAddressaccepts any case and is idempotent. Display-side only — the backend keeps folding. The page<title>was always right (it is built from the route), which is why this only ever showed in the header.
Search adornments, ⌘K assistant, cross-L1 validator count (v1.1.23)
- @luxfi/ui
InputGroupnever applied its measured padding. It measures the start/end adornments with a ResizeObserver and then discards the result: the clone is guarded ondisplayName === 'FieldInput', a component that does not exist in the package, and the props it would set (ps/pe) are Chakra style props that the Tailwind/guiInputspreads onto the DOM and ignores. Both are Chakra→Tailwind port leftovers, so everyInputGroupin the app was unpadded — the header search icon sat on the placeholder's leading letter (measured: 24px left overlap, 52px right, at 1440x900). Fixed at the primitive viapnpm patch→patches/@[email protected](pnpm install --frozen-lockfileapplies it, so CI gets it too). Publish the same fix upstream and the patch can be dropped. 🪤 Removing the dead guard makes the rest of that clone live: it also blankedvalue/placeholderon first render, which flips the field from controlled to uncontrolled. Dropped, and measurement moved to a layout effect so there is no unpadded first frame instead. ClearButtonspreadclassNameover its own visibility classes, so any caller passing one clobberedinvisible— the header showed a clear button on an empty field. Merged withcnat the component.- Validators counted one L1 of four.
useCurrentValidatorsasks a single P-Chain (the oneNEXT_PUBLIC_NETWORK_RPC_URLpoints at). Lux 96369, Hanzo 36963, Zoo 200200 and Pars 494949 each run their own P-Chain with their own validator set — verified 2026-08-06, five each, four disjoint NodeID sets, so 20 not 5.ChainEntry.nodeApiUrlis the per-chain node origin;useNetworkValidatorsfans out through the/v1/pchain?chain=proxy. ⚠️ Pars DOES have a P-Chain and answersplatform.getCurrentValidators— do not assume otherwise. SPC and Osage have no publicly reachable node and are reportedunavailable, never 0. Stake is not summed across chains: each bonds its own currency. - ⌘K assistant. Nothing in this repo,
@luxfi/uior@hanzo/guiships a command palette or chat (nocmdk), so it is the existing Dialog + one global listener +pages/api/ai.ts, which holds the credential server-side. Gated onNEXT_PUBLIC_AI_ASSISTANT_ENABLED;AI_API_KEYis not aNEXT_PUBLIC_var. 🪤 The gui Dialog'ssize={{ lgDown: 'full', lg: 'md' }}leaksfull'smin-h-dvh rounded-noneinto every width —sizeClassesonly prefixes thelghalf. Use a plain size. 🪤 The guiInputsizes itself to 100% and ignores flex utilities; wrap it to make the wrapper the flex item.
🔴 Neither CI plane builds this image — measured 2026-08-06/07
.github/workflows/*isruns-on: lux-build-amd64, andgh api /orgs/luxfi/actions/runnersreturns 0 — those jobs queue forever (the runs on main sat 12–24h and were cancelled)..hanzo/workflows/release.ymlnames a label the fleet DOES carry, but those runners register with git.hanzo.ai, and this repo is a pull mirror there:git push hanzo mainis rejected 3/3 whilegit ls-remote hanzoshows GitHub pushes arriving within ~15s. A mirror sync raises no Actions event, so the workflow never fires.v1.1.23sat tagged with no image for ~2h.- The Gitea REST API 404s on everything, including a repo that exists, so it cannot be used to check run status. Probe GHCR instead, always with a negative control.
- Neither
v1.1.21norv1.1.22carriesNEXT_PUBLIC_GIT_TAGor the OCI labels the release workflow sets — more evidence they were built by hand.
What works: build on our own amd64 nodes.
🪤 A local --platform linux/amd64 cross-build on an arm64 Mac cannot build
this image: colima emulates with QEMU, and pnpm build dies with
QEMU internal SIGILL / uncaught target signal 4.
🪤 buildx's kubernetes driver also fails here — its port-forward drops while
streaming (load build context ... err="EOF"), and when the client dies the
in-pod build keeps burning CPU but nothing is ever pushed.
✅ The reliable path is a Job running buildctl in-cluster, so no local
client is in the loop (scratchpad/buildjob.yaml shape): moby/buildkit
privileged, nodeSelector: runner-pool=1tb + the dedicated=ci-runner
toleration, --opt context=https://github.com/luxfi/explore.git#<tag> so
buildkit clones server-side, and DOCKER_CONFIG pointing at a mounted GHCR
secret so the push happens from inside the cluster.
🪤 Do not read "stalled" from CPU graphs. kubectl top pod reads ~3m CPU /
15Mi for a buildkit pod building flat out — it does not attribute the nested
build cgroup. kubectl top node is no better: the runner-pool-1tb nodes also
host 4 git-runner-* pods. The only honest check is inside the pod —
kubectl exec <pod> -- ps -o pid,etime,time,rss,args; a next build with
14 minutes of CPU TIME and 3.7 GB RSS is working, not hung. Collecting build traces (@vercel/nft) is the long pole on this app, several minutes on its own.
📌 hanzo-build already runs a buildkitd-node DaemonSet (7 nodes, amd64) —
prefer wiring a build to that over creating a namespace and Job by hand as this
note's recipe does.
Runtime ENV plumbing — three places, all required
Adding a NEXT_PUBLIC_* variable needs all of:
configs/app/features/<name>.ts(+ thefeatures/index.tsexport)deploy/tools/envs-validator/schemas/features/<name>.ts(+ itsindex.tsand a.concat()inschema.ts) — the schema is.noUnknown(true)andentrypoint.shvalidates at container start, so an undeclared variable stops the pod booting (unless that deployment setsSKIP_ENVS_VALIDATION, whichexplore-env-luxdoes)docs/ENVS.md—collect_envs.shgreps it to build.env.registry, andcheckPlaceholdersCongruitythrows for a runtime var with no placeholder
🪤 NEXT_PUBLIC_GIT_TAG cannot be set from a ConfigMap. The image bakes a
four-key .env (NEXT_PUBLIC_BRAND, GIT_COMMIT_SHA, GIT_TAG,
ICON_SPRITE_HASH) at build time, and make_envs_script.sh does source .env
before emitting envs.js — so the build-time value wins over the pod env. That
is correct: those four are properties of the image. Verified in-pod on
2026-08-06: printenv NEXT_PUBLIC_GIT_TAG = v1.1.19 while envs.js carried
"". The stray key was the only drift on explore-env-lux (absent from
last-applied-configuration and from both git sources) and was removed,
not adopted — the version must come from the image or the footer can lie about
which build is running.
Shell overflow + white-on-white values + broken Menu button (v1.1.12)
Three write-once-deploy-4 root causes from the 4-breakpoint UI audit. Built
on-cluster (buildkit Job in hanzo-build ns on do-sfo3-hanzo-k8s
runner-pool-32g, git-context clone of luxfi/explore#main), fanned out with
crane to ghcr.io/{luxfi,zooai,hanzoai,parsdao}/explore:v1.1.12 (same digest
sha256:223b3ca2…), rolled to all four explore-fe-* on lux-k8s/lux-mainnet.
Verified LIVE (Playwright/Chromium) 4 brands × {390,768,1280,1920}, 16/16
pass: document.scrollWidth == innerWidth everywhere, all four home values
≥4.5:1, Menu button 32px (lg+)/40px (mobile) with no clip, zero hamburgers at
lg+ (1 at <lg = the correct mobile nav trigger).
- Horizontal overflow (454px @1280, 38px @1920).
Container.tsxforcedmin-w-[100vw] lg:min-w-[fit-content], which promoted the whole page to its ~1734px max-content width (theMainColumngrow shell measured 1734 at 1280) so stat/block cards clipped past the viewport. Fix: fluid column —Container→w-full min-w-0, andMainColumngainsmin-w-0so the flex chain shrinks and the block-rowoverflow-x-autoscrolls internally instead of pushing the page. docSW 1734→1280 / 1958→1920 verified. - White-on-white values (block Reward / tx Value / tx Fee /
network-utilization%, all computed
rgb(255,255,255)@1.04:1). Root cause: legacy Chakra dot-tokens (color="text.secondary",green.600…) were passed straight into an inline CSScolor, which browsers drop as invalid → the text inherited white. Fix: onelib/utils/colorToken.tsresolveColorToken()(dot-token →var(--color-*)) applied inSimpleValue(the value chokepoint for Reward/Value/Fee →#737373= 4.54:1 on the#FAFAFAcard bg), plus new semantic status vars--color-status-good/warn/bad(#1A7A47/#B45309/#B91C1C light = 5.1/4.8/6.1:1; light shades for dark) thatgetNetworkUtilizationParamsnow returns (utilization = 5.13:1). NOTE:@luxfi/uiSkeletonsetsstyle.color = props.colorverbatim — pass it a CSS value/var, never a Chakra dot-token. ~160color="x.y"call sites remain repo-wide; funnel value colors throughSimpleValue. - Broken "Menu" button (
UserWalletButton.tsx): icon+label were stacked in a bare<div>(20px icon over ~20px text) inside a 32pxoverflow:hiddenbutton → the "51px in 32px" clip that read as a stray hamburger at lg+. Fix:flex items-center gap-2+ sized icon (w-5 h-5 shrink-0). Lux shows "Sign in" instead (OIDC auth) — that path was never broken.
Org explorers un-stubbed + mobile detail pages fixed (v1.1.10)
Two mobile-QA defects across the block explorers. DEPLOYED + VERIFIED: image
built on-cluster via buildkit (a Job on do-sfo3-hanzo-k8s
runner-pool-32g — NO GHA; GitHub Actions is disabled for the account) from
commit 7e35a0a, fanned out with crane to ghcr.io/{luxfi,zooai,hanzoai,
parsdao}/explore:v1.1.10 (same digest 6f7c19d0), rolled to all four
explore-fe-* on lux-k8s/lux-mainnet. Verified 390px: docSW==390 (no clip),
labels legible, tab strip scrolls (sw 945, no overlap); desktop 1280px
unchanged; org branding intact (explore.zoo.network = Zoo, networkId 200200,
frontend 1.1.10). CI note: build-lux.yml only builds/deploys luxfi and its
deploy job is currently broken (expired DigitalOcean token → doctl 401);
org rollout is the buildkit+crane path above.
-
Defect 1 (org explorers showed goerli stub data, not their real chains). Root cause was purely TLS:
api-explore.{zoo,hanzo,pars}.networkserved the self-signedCN=INGRESS DEFAULT CERT(no cert-manager Ingress for those SNIs, only lux had one). Real browsers reject it → every FE data fetch fails → react-query falls back to thestubs/placeholders (block #8988736, "Stub Token (goerli)") behind stuck skeletons. The FE wiring was already correct (configmaps point atapi-explore.<org>.network+ right chainId) and the backend already serves real per-chain data — the unifiedexplorerpod has all 6 chains ({"chains":6}) and the org IngressRoutes route by Host /blockscout-prefix-<brand>→/v1/indexer/<slug>(proven: cchain 1.09M blocks, zoo 13,381, hanzo 11, pars 63). Fix = add a dedicated cert-managerletsencrypt-prodIngress per org api host →explorer-unified:8090, mirroringapi-explore-lux-network(declared iningress-api-mainnet.yaml, in the kustomize resources). Certs issued in ~60s; all three nowssl_verify=0. If an org explorer ever goes blank/stubs again, checkkubectl -n lux-mainnet get certificate api-explore-<org>-network-tlsfirst. The@luxfi/uibrowser (Playwright) ignores TLS errors, so it renders real data even when the cert is bad — verify Defect-1-class bugs with a REAL cert-enforcing client (openssl/curl without -k), not the MCP browser. -
Defect 2 (mobile 390px detail pages: dropped labels, horizontal clip, tab overlap). Three shared-UI fixes (fix all 4 explorers at once):
- Labels invisible was the v1.1.9 white-on-white color fix (already landed); deploying v1.1.10 to the org images (they ran v1.0.14) is what makes it show.
- Horizontal clip — the real page-overflow driver at 390px was the FOOTER,
not the field rows.
ui/snippets/footer/Footer.tsxused inlinegridTemplateColumns: 'minmax(auto,470px) 1fr'+repeat(colNum,160px)with no mobile breakpoint → a 470px column past the 390 viewport. Made itgrid-cols-1 lg:grid-cols-[minmax(auto,470px)_1fr](single column on mobile; linksgrid-cols-2via a--footer-colsCSS var for the desktop count). docSW 486→390 verified on a freshnext devrender. - Detail values —
DetailedInfo.ItemValueforcedwhitespace-nowrapwith nomin-w-0, so a long address/hash couldn't shrink and its dynamic shortener (HashStringShortenDynamic, which sizes off its parent width) never bounded. Addedmin-w-0 whitespace-normal break-words overflow-x-hidden lg:whitespace-nowrap lg:overflow-x-visible— desktop unchanged. - Tab bar overlap/collide —
@luxfi/uiTabsList/TabsTriggersilently DISCARD all Chakra style props (overflowX,w,mx,cssscroll-snap,flexShrink, sticky…): they destructure them to_overflowXetc. and only forwardclassName/style. So the mobile scroll +flexShrink={0}the consumer passed never applied → triggers shrank and collided, no scroll. Fixed at the consumer (AdaptiveTabsList.tsx): express the mobile scroll as TailwindclassNameon TabsList (overflow-x-auto snap-x -mx-3 px-3 w-[calc(100%+24px)] lg:…) andclassName="shrink-0 snap-start"on triggers. Verified: tab strip becomes a scroll container (sw 373→921) with no overlap. NOTE for future work: any Chakra-style prop on@luxfi/uiTabs is a no-op — useclassName.DetailedInfo.Container'stemplateColumnsprop is likewise dropped (hardcoded grid); mobile is already single-column so it's benign. - Dev caveat:
next devcrashes tab pages inAdaptiveTabsMenu(pre-existing dev-onlyicons/dots.svg/popover import resolving to object); prod build is fine. Verify tab/detail rendering on the deployed prod URL, footer on dev.
api-explore.lux.network TLS cert fix — explorer data now loads (infra)
The explorer FE was stuck in skeleton loaders ("0/5 CONNECTED", no blocks/txns)
because every api-explore.lux.network request failed in the browser with
ERR_CERT_AUTHORITY_INVALID. Root cause: the explorer Go backend answers by
Host header via the ingress default backend, so the API responded (200) even
with no ingress rule for that host — but with no TLS block for that SNI the
controller served the self-signed CN=INGRESS DEFAULT CERT, which browsers
reject. (curl -k worked; a real browser did not.) A stale
api-explore-lux-network-tls secret existed but nothing referenced/renewed it.
Fix: a dedicated Ingress for api-explore.lux.network → explorer-unified:8090
with a letsencrypt-prod TLS block (mirrors the working
api-explore.osage.network). cert-manager issued a real LE cert (CN=YR2) in
~60s; curl ssl_verify_result=0. Verified live (Playwright): explore.lux.network
now renders 1.09M blocks, 16.9k txns, live blocks + real tx hashes.
Declared in deploy/k8s/explore-fe/ingress-api-mainnet.yaml (in the kustomize
resources) so it survives redeploys. If explorer data ever goes blank, check
this ingress + its cert first (kubectl -n lux-mainnet get certificate api-explore-lux-network-tls). Sibling brands (hanzo/pars/spc) have api-explore
TLS secrets too — if their explorers go blank, they likely need the same
dedicated api-explore ingress.
Native all-chains wallet, lux.id login, menu/panel fix, real deploy path (v1.1.6)
Four owner-reported UI fixes + the deploy/runtime wiring to actually ship them. (v1.1.4 shipped a crash; v1.1.5 fixed it; v1.1.6 fixes the chain switcher.) Verified live on explore.lux.network (Playwright): homepage + /blocks render with live blocks; header menus/panels open with solid backgrounds; Sign in → lux.id login page; chain switcher lists Lux/Zoo/Hanzo/SPC/Pars/Osage.
- Chain switcher didn't open (v1.1.6). ChainSwitcher put onClick={handleToggle} on the PopoverTrigger button, double-toggling against PopoverTrigger's own handler (net: stays closed) — no chains were selectable. Removed the redundant onClick + unused handleToggle (match UserProfileDesktop's pattern).
Flagged, NOT fixed here (need owner input / secrets):
-
Wallet connect is disabled: blockchainInteraction only enables when NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID is set (a real reown/WalletConnect projectId, a vault secret per deploy/values/*.gotmpl). It's absent in the Lux configmap → no wallet-connect UI. brandFamilyChains (the code side of fix c) is in place; enable by adding the projectId secret. Don't use a placeholder.
-
Homepage intermittently error-boundaries ("length" of undefined, after an async update) — pre-existing (old build too); recovers on reload; header + all other pages unaffected. No source maps, couldn't pin the exact widget.
-
CI deploy job: build succeeds on ARC; the deploy job's KMS/DO-token step still needs the DO token (fixed set -e / jq aborts + DIGITALOCEAN_ACCESS_TOKEN fallback). Deploy is done via
kubectl set image explore-fe-lux …:sha-<short>(the documented method) meanwhile. -
App-crash fix (v1.1.4 regression). v1.1.4 changed
useProviderto returnnull(react-query v5 forbidsundefined) but only switched thelib/web3hooks; four UI consumers still didconst { data: { wallet } = {} } = useProvider().= {}defaults onlyundefined, notnull, sonulldata crashed them → whole-page error boundary once account/OIDC put a wallet-using popover in the header. Fixed touseProvider().data ?? {}in ChainWidget, MultichainEcosystemsTableItem, NetworkAddToWallet, AddressAddToWallet. -
Menu / floating panels (the big visible break). The prior build threw ~7k
setReferencecrashes — every popover/menu/drawer (chain switcher, sign-in, mobile nav) rendered transparent (page bled through). Two causes: duplicate@hanzogui/*React contexts (pinned to one 3.0.2 instance) and Tailwind v4 not scanning@luxfi/uiso the panel-surface utilities were purged. Fix:@hanzoguipins +@source "../node_modules/@luxfi/ui"innextjs/global.css- a
--color-popover-bordertoken.
- a
-
Wallet / all-chains.
lib/web3/chains.tsderivesbrandFamilyChainsfromconfigs/app/chainRegistry(+ matching wagmi transports); headerChainSwitcherlists every registry chain on the primary C-Chain explorer.useProviderreturnsnullnotundefined(react-query v5 crashed the wallet hooks — that also broke the menu). -
lux.id login is RUNTIME config, not code. The account feature is gated on env that was never in the deployed ConfigMap, so no sign-in button appeared. Enabled on
explore-env-lux(lux-mainnet) +deploy/k8s/explore-fe/configmap-mainnet.yaml:IS_ACCOUNT_SUPPORTED=true,ACCOUNT_AUTH_PROVIDER=oidc,OIDC_SERVER_URL=https://lux.id,OIDC_CLIENT_ID=lux-explore-client-id(issuer live; authorizehttps://lux.id/v1/iam/oauth/authorize). -
Deploy path (was broken). explore.lux.network is served by Deployment
explore-fe-lux(nslux-mainnet,ghcr.io/luxfi/explore:sha-<short>, IfNotPresent).build-lux.ymlbuilds on thelux-buildARC pool; the deploy job nowkubectl set image explore-fe-luxto the freshly built sha + waits on rollout — it previously restarted a non-existentlux-frontend-mainnetand its KMSDO_API_TOKENfetch returned empty (deploys silently skipped). Added aDIGITALOCEAN_ACCESS_TOKENfallback; footer version now from package.json. -
Manifest.
scripts/generate-well-known.jsregeneratespublic/.well-known/explore.jsonat build; now emits<Brand> Network, never "Subnet".
chainId reconciliation + RPC → /v1 (v1.1.7): registry now matches the LIVE
chains AND the canonical genesis source of truth
(~/work/lux/genesis/configs/lp182_chain_id_map.go, the LP-018 (family,env)→
EVMChainID map). One value, one way.
- Pars mainnet 494949 (was the stale 7070) —
api.pars.networkreportseth_chainId=0x78d65/net_version=494949. Pars testnet 494950, devnet 494951 (were 7071/7072) — matches the LP-018 map's 4949xx theme. Note the LP-018 map itself was ALSO stale on Pars mainnet (7070) and was corrected to 494949 in the genesis repo; testnet/devnet 494950/494951 were already canonical there. - Lux devnet 96367 (was the stale 96370) —
api.lux-dev.networkreports 96367. Canonical Lux: mainnet 96369 / testnet 96368 / devnet 96367. chainRegistry.spec.tsnow asserts Pars 494949 (was the stale-7070 assert).- Fixed in:
configs/app/chainRegistry.ts,chainRegistry.spec.ts,public/.well-known/explore.json,lib/web3/chains.ts(comment),deploy/k8s/explore-fe/configmap-{mainnet,testnet}.yaml,deploy/scripts/branded_build.sh,tools/send_test_txs.py. - Chain RPC migrated
/v1/chain/<alias>/rpc→/v1/chain/C/rpc(gateway canonical; every sovereign L1 exposes its own C-Chain at/v1/chain/C/rpc, so zoo/hanzo/pars drop their old brand-aliased/v1/bc/{zoo,hanzo,pars}/rpcpaths — which were already 404ing). Verified returning blocks BEFORE switching: Lux/Zoo/Hanzo/Pars mainnet + Lux testnet. Deliberately still on/ext(their/v1gateway route is not up yet — do NOT switch until a block returns): Lux devnet, Zoo/Hanzo/Pars testnet, and the local-node compose +send_test_txs.pyURLs (raw luxd serves/extnatively, no gateway). P-chain proxy (pages/api/pchain.ts) now dials/v1/bc/P(verified).useChainHeights.tsandluxnet/instance.tsno longer strip/v1/bc/C/rpc: they use the env RPC URL (or its origin) directly, so the C-chain height widget + luxnet SDK work under either path scheme.
Chain-visibility + 10-VM/DEX handling (v1.0.14)
Two orthogonal correctness fixes shipped in one build cycle.
- Chain-visibility rule (PR #7):
isPrimaryNetworkExplorer()now requires!isWhiteLabelMode() && vm === 'EVM', so unregistered/brand hosts stop rendering the Lux primary panels + cross-L1 list. Registeredexplore.zoo.network/.ngo+explore.pars.networkas own-scoped L2s (brand parity with Hanzo), added Osage L1, and set Pars chainId (interim 7070, since reconciled → 494949 in v1.1.7 above). Rule: lux explorer = ALL chains; hanzo/zoo/pars = ONLY their own. Proven byconfigs/app/chainRegistry.spec.ts(12 cases). - 10-VM / DEX handling: the Lux primary network is a family of VM-specialized
chains (C=EVM, X=UTXO, D=DEX, P=platform, + A/B/Q/T/Z/G/K/O/R/I/M), mirrored
from
~/work/lux/node/node/vms.go. New single source of truthconfigs/app/primaryChains.ts(PRIMARY_VMS,getPrimaryVm,view,hasBespokeView,chainsAwaitingBespokeView) decomplects the two lists that used to duplicate this (ChainsPage / ChainDetailPage now consume it). Each VM routes at/chains/<slug>;viewselects the render:- D-Chain → the DEX order-book UI (
ui/dex/DexPage, wired to the indexer./dexadapter vialib/api/dchain) —ChainDetailPagerenders<DexPage/>forview: 'dex', not a generic block list. - C-Chain = the EVM explorer core; P-Chain = platform/validators data;
X-Chain + the custom VMs (A/B/Q/T/Z/G/K/O/R/I/M) render the generic
chain-detail fallback (info + indexer DAG stats + validators) — they do NOT
crash/blank.
chainsAwaitingBespokeView()FLAGS the backlog: X-Chain (UTXO) and the 11 custom VMs still need a bespoke explorer view; only C/P/D have one today.
- D-Chain → the DEX order-book UI (
- VMs are Lux-primary → lux-only: the primary-VM surfaces (Chains / DEX /
Bridge / AI Compute nav + the
/chains,/chains/[slug],/dexpages) are gated onisPrimaryNetworkExplorer(). Nav hides them on brand/white-label hosts;ui/shared/PrimaryNetworkGuardbackstops direct-URL access so the 10 VMs can never leak onto Hanzo/Zoo/Pars. Proven byconfigs/app/primaryChains.spec.ts(26 cases). - Build/deploy: single multi-destination kaniko Job (NO GHA), context
git://github.com/luxfi/explore.git#main, Dockerfile (base Next.js build), fan-out--destination=ghcr.io/{luxfi,zooai,hanzoai,parsdao}/explore:v1.0.14(parsdao ADDED — it had no v1.0.1x image). White-label is 100% runtime via theexplore-env-{brand}configmaps (APP_HOST), not baked per-brand. Deploy = bump all fourexplore-fe-*images to v1.0.14 inluxfi/universe k8s/lux-mainnet/explore-fe.yaml, operator reconcile.
Realtime transport: Phoenix WebSocket → SSE (v1.0.13)
- Bug: the FE connected realtime via Phoenix Channels
(
wss://api-explore.lux.network/socket/v2/websocket), but the Go backend (luxfi/explorer) serves no Phoenix endpoint — its realtime is SSE at/v1/base/realtime(multiplexed, envelope{event, chain, data}, initialevent: CONNECT,: pingkeep-alive). Result: repeatingws 404→ "Live updates temporarily delayed". This is a frontend-only transport mismatch; the chain/indexer are untouched. - Fix:
lib/socket/sse.tsreimplements the slice of the PhoenixSocket/ChannelAPI the 27 realtime consumers use, backed by ONEEventSource. The seam is unchanged —SocketProvider/useSocketChannel/useSocketMessageand every consumer keep working; only the transport underneath swapped. No morephoeniximport in app code. - Routing: envelope
{event, chain, data, topic?}. Withtopic→ delivered straight to that channel/event (scopedaddresses:*/tokens:*, and the component-test harness). Without → theTRANSLATIONStable maps the backend hub channel to a Blockscout(topic, event); today the indexer broadcasts onlyblocks→blocks:new_block/new_block.join()resolvesokon stream open; EventSource auto-reconnects. URL normaliser collapses anywss://…/socket*(or testws://host:port) to<https>/v1/base/realtime. - Tests:
lib/socket/sse.spec.ts(12 vitest cases, all green) covers URL normalisation, blocks→new_block translation, direct-topic routing, joinok, on/off, onError, disconnect. CT harnessplaywright/fixtures/socketServer.tsis now a tiny SSE server (samecreateSocket/joinChannel/sendMessagesignatures) so the 12 consumer specs are unchanged. - Still owner-gated (separate, NOT this fix): data freshness on
explore.lux.network is blocked on the C-Chain rollback + indexer reorg
handling — no fresh blocks flow until that lands. When it does, align the
EVMBlock→Blockscout
Blockfield shapes in theblockstranslation (or have the backend emit Blockscout-shaped blocks); that is the only remaining piece and it can only be verified end-to-end once data flows again.
Investor-grade data: real validators + honest-empty stats/DEX (v1.0.11)
Removed every fabricated placeholder; data is now real-from-chain or honestly empty, never faked. Root causes + fixes:
- Validators/Stake = 0 was a proxy bug, not missing data. P-chain
platform.getCurrentValidatorsreturns real validators (5, total weight 2.5e18 on hanzo).pages/api/pchain.tsderived its base URL by stripping ONLY/v1/chain/C/rpcvia regex; brand RPCs are…/v1/chain/<chain>/rpc(hanzo:/v1/chain/hanzo/rpc), so the regex no-op'd and the proxy POSTed to…/v1/bc/hanzo/rpc/v1/bc/P→ HTML 404 → "Unexpected non-whitespace character after JSON at position 4" → 502. Fix:new URL(rpcUrl).origin(works for every brand) + defensive text→JSON parse with a clear error.ui/stats/lux/NetworkStats.tsx(Network Overview: Total Chains / Validators / Connected / Total Stake / Avg Uptime) already had honest—fallbacks and now renders REAL values.lib/api/luxnet/instance.tswas already safe (usesnew URL().hostname), so B/D-Chain SDK calls were unaffected. - "Placeholder Counter 9.074M ×8" came from
stubs/stats.tsSTATS_COUNTER(value:'9074405', title:'Placeholder Counter') used as React-QueryplaceholderDatainui/stats/NumberWidgetsList.tsx. When the Blockscout stats microservice is absent (hanzo has NONEXT_PUBLIC_STATS_API_HOST; everyapi-explore.hanzo.network/api/v1|v2/*404s) the stub leaked as "real". Fix: neutralize the stub (title:'', value:'0') AND gate the counters/charts inui/pages/Stats.tsxonconfig.features.stats.isEnabled— show the live chain-sourced Network Overview always, with an honest "being indexed" note when no stats service. Aggregate counters (total txns/addresses, gas-used- today, history charts) genuinely need a stats indexer that isn't deployed. - DEX showed static demo numbers.
lib/api/dchain/useDexData.tshad hardcodedDEMO_SYMBOLS/ORDERS/TRADES/POOLS/OVERVIEW(LUX/USDT 24.85, etc.) served when the D-Chain (DexVM) is unavailable. Deleted ALL demo data → real D-Chain data or honest-empty;ui/dex/DexPage.tsxrenders an honest empty/ error message per tab. - Bridge signers = 0 is already honest (
lib/api/bchain/useBridgeData.tsreturns real B-Chain signer set orEMPTY_STATS; B-Chain not yet reporting). - Homepage is already investor-grade: real RPC blocks (#8–#13), tx hashes,
gas (2.5 Gwei), Total blocks/txns/wallets —
ui/home/Stats.tsxdegrades to RPC when the general indexer errors;HOMEPAGE_STATSstub is placeholder-only. - Blockchain dropdown clipped its last item ("Verified Contracts"):
@luxfi/uiTooltipvariant="popover"Content defaults tooverflow-hidden px-3 py-2, clipping a tall menu<ul>. Fixed at the consumer (ui/snippets/navigation/horizontal/NavLinkGroup.tsx) viacontentProps={{ className:'overflow-visible px-1.5 py-2' }}(twMerge wins)placement:'bottom-start'. No node_modules patch.
- Still needs a real indexer (honest-empty until then): aggregate Stats counters + history charts (Blockscout stats microservice), DEX live markets/orders/trades (DexVM indexer), Bridge signer set (B-Chain).
- Built v1.0.11 via the kaniko Job pattern (NO GHA) →
ghcr.io/{hanzoai,zooai, luxfi}/explore:v1.0.11; deploy = image patch ofexplore-fe-*Deployments on do-sfo3-lux-k8s nslux-mainnet(plain Deployments, not operator-managed).
Validators page: visible in light mode + brand currency (v1.0.12)
v1.0.11 fetched REAL P-chain validators (5, total weight 2.5e18 on hanzo) but
a Playwright pass found the /validators view rendered white-on-white in
light mode — the data was present in the DOM yet invisible to users (only the
tab bar showed because it alone used text-[var(--color-text-primary)]). The
three custom Lux components (ui/validators/lux/{ValidatorsDashboard, ValidatorsList,DelegatorsList}.tsx) set NO text color, so they inherited a
white color that doesn't flip with the theme. Fixes:
- Set
text-[var(--color-text-primary)]on each component's root container — one place per component; the token flips perstyles/tokens.css(lightrgba(16,17,18,.80)/ darkrgba(255,255,255,.80)), so all descendants now render the mode-correct color (dark on light, light on dark). - Stake unit was hardcoded
LUXon every brand; nowconfig.chain.currency. symbol || 'LUX'(=AIon Hanzo) — matchesNetworkStats.tsx's pattern, DRY across all 6 stake labels in the three components. - NOT changed:
formatStakeusesLUX_DECIMALS = 6, the codebase-wide convention (NetworkStats + ChainDetailPage use the same); P-chaingetMinStakereturnsminValidatorStake:2000000000(= 2000 at 6dp), consistent with 6. (Three copies of this helper is a latent DRY violation to consolidate later, but the value is correct — left as-is to avoid drift.) next.config.jshastypescript.ignoreBuildErrors:true; repo carries ~224 pre-existing tsc errors (all in*.pw.tsx/ unrelated) — build is unaffected. Dockerfile uses pnpm (not yarn). Edited files lint+typecheck clean.- Built v1.0.12 via the kaniko Job (NO GHA) →
ghcr.io/{hanzoai,zooai,luxfi}/ explore:v1.0.12; deployed by image patch ofexplore-fe-{hanzo,lux,zoo}on do-sfo3-lux-k8s nslux-mainnet. (Built on do-sfo3-hanzo-k8s nshanzo.)
Multi-brand hostname-driven header logo (explore v1.0.10)
- ONE image white-labels by request Host:
configs/app/chainRegistry.tsgetCurrentChain()resolves the chain (and its brand) from the hostname, and the header renderschain.branding.logoContent(an inline-SVG mark usingcurrentColor). NO hardcoded logo, no image-URL→triangle fallback. - Per-brand marks live in
chainRegistry.ts: Lux = downward triangle (viewBox0 0 100 100), Hanzo = real blocky-H (@hanzo/logo, viewBox0 0 67 67, 5 paths —M22.21 67…), Zoo = interlocking circles (0 0 1024 1024), Pars = 8-pointed star, SPC = unicorn. - The header logo renders in THREE spots, all reading
branding:ui/snippets/topBar/TopBar.tsx(desktop), andui/snippets/networkLogo/{NetworkLogo,NetworkIcon}.tsx(mobile headerui/snippets/header/HeaderMobile.tsx+ chain-switcher). The stale pre-v1.0.10 mobileNetworkIconhardcoded aviewBox="0 0 50 50"<polygon>triangle mislabeledaria-label="Hanzo icon"— fixed in4369780(mobile) +79dbe63(desktop, single header per breakpoint). - Multi-tenant L1/L2/L3: each
ChainEntryhas avmfield (EVM= Lux primary L1,L2= brand sovereign chains) gatingisPrimaryNetworkExplorer()so brand explorers show ONLY their own chain. mainnet/testnet/devnet/localnet entries per brand; hostnames cover both canonical (explore-<brand>.lux.network) and brand-domain (explore.<brand>.network,explore.hanzo.ai) hosts.
Build + deploy (NO GitHub builders)
- Built on-cluster via kaniko (do-sfo3-hanzo-k8s ns
hanzo, node poolrunner-pool-32g, secretkaniko-ghcr-multi, tolerationdedicated=ci-runner): contextgit://github.com/luxfi/explore.git#main,--dockerfile Dockerfile, fan-out--destinationto per-brand GHCR orgsghcr.io/{luxfi,zooai,hanzoai}/explore:v1.0.10(one digest,sha256:4f980291…). pars (ghcr.io/parsdao/explore) not in the fan-out yet → stays v1.0.4. - Runs on do-sfo3-lux-k8s ns
lux-mainnetasexplore-fe-<brand>Deployments (selector{app:explore-fe,brand,network}), env fromexplore-env-<brand>ConfigMap, pull secretghcr-luxfi, svc 80→3000. NOT operator-managed (plain Deployments) → bump via image patch / apply. Declared state:luxfi/universe k8s/lux-mainnet/explore-fe.yaml(+k8s/lux-devnet/explore-fe.yaml). - App hydrates client-side (SSR HTML is an 8 KB shell) — verify the logo
with a real browser (Playwright), not
curl | grep.
Auth — Hanzo IAM is the only gate (do not add a second)
- Sign-in is OIDC against Hanzo IAM. Never build local password/OTP/SIWE
auth, and never re-point Blockscout's
/account/*auth at our backend — rewriting it is building custom auth. The auth0 (email + one-time-code + reCAPTCHA), Dynamic.xyz and CSRF paths were deleted, not ported: every/account/*endpoint 404s onapi-explore.*(measured), so none of it could ever have worked here. lib/oidc.tsis the ONE place that knows IAM's endpoints. Never rebuild a path at a call site. IAM publishes discovery at<issuer>/.well-known/openid-configuration; everything lives under/v1/iam/oauth/*(authorize,token,userinfo,logout). The bare/oauth/*and/api/userinfopaths are the IdP's SPA shell or a 404 and fail silently.- PKCE is mandatory. IAM rejects a public client without it:
error=invalid_request, PKCE is required for public clients. Our clients are public (a browser holds no secret), so the S256 challenge is not optional polish — it is the thing that makes login work at all. - Client ids are
<org>-<app>(HIP-0111):lux-explore,zoo-explore,hanzo-explore,pars-explore. The tree shippedNEXT_PUBLIC_OIDC_CLIENT_ID=lux-explore-client-id, a client that never existed →400 unknown client_id. Issuers arelux.id,zoolabs.id(NOTzoo.id— it does not resolve),hanzo.id,pars.id. - IAM state is changed ONLY by
make iam-provisionfrom that org'suniverse/infra/k8s/iam/provision.yaml. 🪤 The upsert REPLACESredirectUris— it does not merge. Any live URI not derivable fromhosts:must be stated underredirects:or the next converge deletes it.
UI stack — the migration is already done; Tailwind is NOT removable
- Measured:
@chakra-ui0 files,@emotion0,styled-system0,@radix-ui0 files in our code.@luxfi/uiis imported by ~787 files. - The ~17
@radix-ui/*entries inpackage.jsonhave zero call sites — they duplicate@luxfi/ui's own 13 Radix dependencies. Dead declarations, safe to drop, but they are our library's internals and Radix stays in the tree. - Tailwind v4 is structural, not legacy cruft.
nextjs/global.csshas@source "../node_modules/@luxfi/ui"precisely so Tailwind emits the utilities@luxfi/uiitself uses; purge them and the library breaks. There is notailwind.config— v4 is configured in CSS (@theme,@custom-variant). - Real remaining debt: ~137 files still pass Chakra-shaped style props
(
fontSize="sm",w,borderRadius) that@luxfi/uiabsorbs via anany-typed compat shim. If that shim goes, those files break.
Deployment traps
- The four
ghcr.io/{luxfi,hanzoai,zooai,parsdao}/exploretags are byte- identical copies (same digest) — brand comes entirely from theexplore-env-<brand>ConfigMap at runtime. A version bump is thereforecrane copy, not four builds. Never mix org registries. - 🪤
tsconfig.jsonhasincremental: true. A staletsconfig.tsbuildinforeports errors against file contents that no longer exist. Delete it before trusting a typecheck diff. - 🪤
.cspell-words.txtis in cspell's ignorePaths, not a dictionary. New words go in thewordsarray incspell.jsoncor the pre-commit hook fails.
The primary-network chains: what is real, and what only looks it
Measured 2026-08-08 against all three networks. These are facts about the network, not about this app, so check them before believing any chain page.
- The chain list is
GET /v1/chain/P/ops/blockchains, never a table in this repo. mainnet and testnet report nine chains (A B C D G K Q X Z); devnet reports ten, adding M.configs/app/primaryChains.tsadditionally names T, R, I and O, which are registered on NO network — the node answers "there is no ID with alias: T". That table is presentation metadata; the network is the source of truth./chainsand/chains/<slug>join the two. - The public gateway routes exactly three chains:
/v1/chain/P,/v1/chain/Xand/v1/chain/C/rpc. Everything else 404s — including C-Chain's own blockchain ID,/v1/bc/25td8att…/rpc, which proves the 404 is the route and not the chain. D, A, B, Z, G, K are registered AND bootstrapped on the node; a browser simply cannot reach them. Render that as unreachable, never as a height of 0. - Q-Chain is
isBootstrapped: falseon all three networks and is the sole entry in/v1/health's not-bootstrapped list — which is why all three networks reporthealthy: false. Q's page says Syncing; that is correct. - P and X both report height 0 on all three. That is the RPC's real answer (genesis-era P timestamps confirm it), not a failure.
- X speaks
xvm.*, notavm.*.avmreturns "can't find service".xvm.getBlockchainIDandxvm.getTxFeeare not in this build either. - C-Chain chain ids: mainnet 96369, testnet 96368, devnet 96367.
- 🪤
api-indexer-<x>chain.lux.network— all fifteen were NXDOMAIN. Deleted. Do not reintroduce a per-chain indexer host map without probing DNS first.
Reaching the node
/v1/node/<endpoint> (pages/api/node/[endpoint].ts) is the ONE proxy. The
browser cannot call the node directly — the gateway 404s the OPTIONS preflight
for /v1/chain/*. The endpoint is resolved against the primary-chain table and
never used as a URL. A chain the gateway does not route answers 200 with a
JSON-RPC error, not 502: nothing is broken, and 502 painted a failed request
into the console of eight of the ten chain pages.
🪤 Deployment does not follow the tag
.hanzo/workflows/release.yml builds the image and copies it to each brand
registry, then prints "Pin all five brands to in luxfi/universe
deploy/lux-mainnet/". Nothing performs that pin. On 2026-08-08 GHCR held
v1.1.28, v1.1.29 and v1.1.26 while universe still declared v1.1.26 — so
v1.1.27 (never built) and v1.1.28/29 (built, undeployed) had all been sitting.
Moving the pin is a hand commit in luxfi/universe; the history does exactly
that ("move all five brands to v1.1.26").
🪤 Only deploy/lux-mainnet/explore-fe-*.yaml drives CD (cd.automated: true,
"CD applies this file within ~90s"). The testnet and devnet files are
snapshots generated from the live Deployment by hack/live2values.py and say
so in their header — editing them deploys nothing. Those two clusters were on
v1.0.4 / sha-caf792d while mainnet ran v1.1.26.
2024-12-24
- Merged upstream blockscout/frontend (up to commit 5a49ad8b1)
- Updated branding from Blockscout to Lux Network
- Added logo assets
Cleanup
- Removed 8 stale
*.orig/*.bakbackups (~1.9 MB) left by the Blockscout→Lux rebrand commits; two still carried raw<<<<<<< HEADconflict markers. Nothing referenced them.*.orig/*.rej/*.bakare now in.gitignore(upstream's had*.orig; the fork dropped it, which is how they got committed).
Note: This file is tracked in git. Update it when making significant architectural changes or discoveries about the codebase.