Files
2026-09-11 13:50:40 -07:00

876 lines
52 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Lux Network Explorer - AI Knowledge Base
**Last Updated**: 2024-12-24
**Project**: explore
**Organization**: luxfi
**Upstream**: [blockscout/frontend](https://github.com/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.
```bash
# 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:
1. `rm -rf public/assets/{configs,multichain,envs.js}`
2. `download_assets.sh` — skips every unset `NEXT_PUBLIC_*` asset URL, so it is
offline-safe with the committed local env files
3. `build_sprite.sh` — needs `jq` and `md5sum` (not macOS defaults)
4. **`make_envs_script.sh` → `public/assets/envs.js`** — this is the file the
browser reads. `pages/_document.tsx` loads it as a blocking `<script>` and
`configs/app/utils.ts:getEnvValue` reads `window.__envs` from it. Plain
`next dev` does NOT generate it, and without it every API-backed page throws
`API config for general not found`.
5. `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.
```bash
# 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):
```bash
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 customization
- `public/logos/` - Logo assets
- `toolkit/theme/` - Theme colors
## Development Workflow
### Syncing with Upstream
```bash
# 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
1. Check if feature exists in upstream Blockscout
2. If not, implement in `ui/` or `lib/` directories
3. Add TypeScript types in `types/`
4. Add mock data in `mocks/` for testing
5. Test with Playwright
### Lux-Specific Changes
Key Lux customizations:
- `ui/snippets/networkLogo/` - Lux logo component
- `toolkit/theme/foundations/` - Lux color palette
- `public/logos/` - Lux brand assets
- `Dockerfile.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.md`
- `CLAUDE.md`
- `QWEN.md`
- `GEMINI.md`
All symlinks reference this single source of truth.
## Rules for AI Assistants
1. **ALWAYS** update LLM.md with significant discoveries
2. **PRESERVE** Lux branding when merging upstream changes
3. **DO NOT** modify Blockscout-specific CI workflows
4. **USE** `pnpm` for package management (repo is `packageManager: [email protected]`; npm/yarn break the `@hanzogui` overrides)
5. **TEST** UI changes with Playwright before committing
## Recent Changes
### Duplicated tokens filter, colliding footer row, lower-case addresses (v1.1.27)
- **`ui/tokens/TokensActionBar.tsx` was 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 a `display={{base:
'none', lg:'flex'}}` block inside the sticky `ActionBar` (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 no `flex`, their
children stacked instead of sitting in a row. Every sibling kept the pattern —
`VerifiedContracts`, `NameDomainsActionBar`, `ClustersActionBar` all use
`flex 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.
⚠️ `NameDomainsActionBar` writes its desktop block as `flex hidden lg:block` —
`block`, so its filter and input still stack at lg. Same family, not fixed here.
- **@luxfi/ui's `ButtonFrame` never sets `flexDirection`.** It is `styled(View)`
on a react-native `View`, whose default is `column`, while the frame sets
`inline-flex`, `alignItems`, `gap`, a fixed `height` and `overflow: 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 only
`Filter` clipped, so this is fixed at `ui/shared/filters/FilterButton.tsx`
(the single component behind every filter in the app) with an inline
`style={{ flexDirection: 'row' }}` — inline because the frame's own
`is_View` class outranks a Tailwind `flex-row` utility. **The primitive is
still wrong**; fix `ButtonFrame` upstream and drop the call-site style.
- **`Footer`'s no-custom-links branch never wrapped its left column.** The grid is
`lg:grid-cols-[minmax(auto,470px)_1fr]` but that branch passes
`renderNetworkInfo() / 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 mobile
`grid-cols-2` tracks were also still the only *explicit* tracks at `lg`, so they
took `1fr` each, 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 add `lg: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. Compare `max(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.tsx` and
`ui/multichain/address/MultichainAddress.tsx` both read
`addressQuery.data?.hash ?? getCheckedSummedAddress(hash)`, and the `??` took
the lower-case payload whenever the address existed. Both now checksum whatever
they display; `getAddress` accepts 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 `InputGroup` never applied its measured padding.** It measures the
start/end adornments with a ResizeObserver and then discards the result: the
clone is guarded on `displayName === '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/gui `Input` spreads onto the DOM and ignores. Both are
Chakra→Tailwind port leftovers, so **every** `InputGroup` in 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 via `pnpm patch` → `patches/@[email protected]`
(`pnpm install --frozen-lockfile` applies 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 blanked
`value`/`placeholder` on 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.
- **`ClearButton` spread `className` over its own visibility classes**, so any
caller passing one clobbered `invisible` — the header showed a clear button on
an empty field. Merged with `cn` at the component.
- **Validators counted one L1 of four.** `useCurrentValidators` asks a single
P-Chain (the one `NEXT_PUBLIC_NETWORK_RPC_URL` points 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.nodeApiUrl` is the per-chain node origin;
`useNetworkValidators` fans out through the `/v1/pchain?chain=` proxy.
⚠️ **Pars DOES have a P-Chain** and answers `platform.getCurrentValidators` —
do not assume otherwise. SPC and Osage have no publicly reachable node and are
reported `unavailable`, never 0. Stake is **not** summed across chains: each
bonds its own currency.
- **⌘K assistant.** Nothing in this repo, `@luxfi/ui` or `@hanzo/gui` ships a
command palette or chat (no `cmdk`), so it is the existing Dialog + one global
listener + `pages/api/ai.ts`, which holds the credential server-side. Gated on
`NEXT_PUBLIC_AI_ASSISTANT_ENABLED`; `AI_API_KEY` is **not** a `NEXT_PUBLIC_`
var. 🪤 The gui Dialog's `size={{ lgDown: 'full', lg: 'md' }}` leaks `full`'s
`min-h-dvh rounded-none` into every width — `sizeClasses` only prefixes the
`lg` half. Use a plain size. 🪤 The gui `Input` sizes 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/*` is `runs-on: lux-build-amd64`, and
`gh api /orgs/luxfi/actions/runners` returns **0** — those jobs queue forever
(the runs on main sat 12–24h and were cancelled).
- `.hanzo/workflows/release.yml` names 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 main` is rejected 3/3 while `git ls-remote hanzo` shows
GitHub pushes arriving within ~15s. A mirror sync raises no Actions event, so
the workflow never fires. `v1.1.23` sat 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.21` nor `v1.1.22` carries `NEXT_PUBLIC_GIT_TAG` or 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:
1. `configs/app/features/<name>.ts` (+ the `features/index.ts` export)
2. `deploy/tools/envs-validator/schemas/features/<name>.ts` (+ its `index.ts`
and a `.concat()` in `schema.ts`) — the schema is `.noUnknown(true)` and
`entrypoint.sh` validates at container start, so an undeclared variable
**stops the pod booting** (unless that deployment sets `SKIP_ENVS_VALIDATION`,
which `explore-env-lux` does)
3. `docs/ENVS.md` — `collect_envs.sh` greps it to build `.env.registry`, and
`checkPlaceholdersCongruity` **throws** 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.tsx` forced
`min-w-[100vw] lg:min-w-[fit-content]`, which promoted the whole page to its
~1734px max-content width (the `MainColumn` grow shell measured 1734 at 1280)
so stat/block cards clipped past the viewport. Fix: fluid column —
`Container` → `w-full min-w-0`, and `MainColumn` gains `min-w-0` so the flex
chain shrinks and the block-row `overflow-x-auto` scrolls 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 CSS `color`, which browsers drop as invalid → the text
inherited white. Fix: one `lib/utils/colorToken.ts` `resolveColorToken()`
(dot-token → `var(--color-*)`) applied in `SimpleValue` (the value chokepoint
for Reward/Value/Fee → `#737373` = 4.54:1 on the `#FAFAFA` card 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) that
`getNetworkUtilizationParams` now returns (utilization = 5.13:1).
NOTE: `@luxfi/ui` `Skeleton` sets `style.color = props.color` verbatim — pass
it a CSS value/var, never a Chakra dot-token. ~160 `color="x.y"` call sites
remain repo-wide; funnel value colors through `SimpleValue`.
- **Broken "Menu" button** (`UserWalletButton.tsx`): icon+label were stacked in
a bare `<div>` (20px icon over ~20px text) inside a 32px `overflow:hidden`
button → 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}.network` served the
self-signed `CN=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 the `stubs/` placeholders (block #8988736, "Stub
Token (goerli)") behind stuck skeletons. The FE wiring was already correct
(configmaps point at `api-explore.<org>.network` + right chainId) and the
backend already serves real per-chain data — the unified `explorer` pod 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-manager
`letsencrypt-prod` Ingress per org api host → `explorer-unified:8090`,
mirroring `api-explore-lux-network` (declared in `ingress-api-mainnet.yaml`,
in the kustomize resources). Certs issued in ~60s; all three now
`ssl_verify=0`. **If an org explorer ever goes blank/stubs again, check
`kubectl -n lux-mainnet get certificate api-explore-<org>-network-tls` first.**
The `@luxfi/ui` browser (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.tsx` used inline
`gridTemplateColumns: 'minmax(auto,470px) 1fr'` + `repeat(colNum,160px)` with
no mobile breakpoint → a 470px column past the 390 viewport. Made it
`grid-cols-1 lg:grid-cols-[minmax(auto,470px)_1fr]` (single column on mobile;
links `grid-cols-2` via a `--footer-cols` CSS var for the desktop count).
docSW 486→390 verified on a fresh `next dev` render.
- *Detail values* — `DetailedInfo.ItemValue` forced `whitespace-nowrap` with no
`min-w-0`, so a long address/hash couldn't shrink and its dynamic shortener
(`HashStringShortenDynamic`, which sizes off its parent width) never bounded.
Added `min-w-0 whitespace-normal break-words overflow-x-hidden lg:whitespace-nowrap
lg:overflow-x-visible` — desktop unchanged.
- *Tab bar overlap/collide* — **`@luxfi/ui` `TabsList`/`TabsTrigger` silently
DISCARD all Chakra style props** (`overflowX`, `w`, `mx`, `css` scroll-snap,
`flexShrink`, sticky…): they destructure them to `_overflowX` etc. and only
forward `className`/`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
Tailwind `className` on TabsList (`overflow-x-auto snap-x -mx-3 px-3
w-[calc(100%+24px)] lg:…`) and `className="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/ui` Tabs is a no-op —
use `className`. `DetailedInfo.Container`'s `templateColumns` prop is likewise
dropped (hardcoded grid); mobile is already single-column so it's benign.
- Dev caveat: `next dev` crashes tab pages in `AdaptiveTabsMenu` (pre-existing
dev-only `icons/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 `useProvider` to return
`null` (react-query v5 forbids `undefined`) but only switched the `lib/web3`
hooks; four UI consumers still did `const { data: { wallet } = {} } =
useProvider()`. `= {}` defaults only `undefined`, not `null`, so `null` data
crashed them → whole-page error boundary once account/OIDC put a wallet-using
popover in the header. Fixed to `useProvider().data ?? {}` in ChainWidget,
MultichainEcosystemsTableItem, NetworkAddToWallet, AddressAddToWallet.
- **Menu / floating panels (the big visible break).** The prior build threw
~7k `setReference` crashes — 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/ui` so the panel-surface utilities were purged. Fix:
`@hanzogui` pins + `@source "../node_modules/@luxfi/ui"` in `nextjs/global.css`
+ a `--color-popover-border` token.
- **Wallet / all-chains.** `lib/web3/chains.ts` derives `brandFamilyChains`
from `configs/app/chainRegistry` (+ matching wagmi transports); header
`ChainSwitcher` lists every registry chain on the primary C-Chain explorer.
`useProvider` returns `null` not `undefined` (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; authorize `https://lux.id/v1/iam/oauth/authorize`).
- **Deploy path (was broken).** explore.lux.network is served by Deployment
`explore-fe-lux` (ns `lux-mainnet`, `ghcr.io/luxfi/explore:sha-<short>`,
IfNotPresent). `build-lux.yml` builds on the `lux-build` ARC pool; the deploy
job now `kubectl set image explore-fe-lux` to the freshly built sha + waits on
rollout — it previously restarted a non-existent `lux-frontend-mainnet` and
its KMS `DO_API_TOKEN` fetch returned empty (deploys silently skipped). Added
a `DIGITALOCEAN_ACCESS_TOKEN` fallback; footer version now from package.json.
- **Manifest.** `scripts/generate-well-known.js` regenerates
`public/.well-known/explore.json` at 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.network` reports
`eth_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.network` reports
96367. Canonical Lux: mainnet 96369 / testnet 96368 / devnet 96367.
- `chainRegistry.spec.ts` now 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}/rpc` paths — which were
already 404ing). Verified returning blocks BEFORE switching: Lux/Zoo/Hanzo/Pars
**mainnet** + Lux **testnet**. Deliberately still on `/ext` (their `/v1` gateway
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.py`
URLs (raw luxd serves `/ext` natively, no gateway). P-chain proxy
(`pages/api/pchain.ts`) now dials `/v1/bc/P` (verified). `useChainHeights.ts`
and `luxnet/instance.ts` no 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. Registered
`explore.zoo.network`/`.ngo` + `explore.pars.network` as 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 by `configs/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 truth
`configs/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>`; `view` selects the render:
- **D-Chain → the DEX order-book UI** (`ui/dex/DexPage`, wired to the indexer
`./dex` adapter via `lib/api/dchain`) — `ChainDetailPage` renders `<DexPage/>`
for `view: '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.
- **VMs are Lux-primary → lux-only:** the primary-VM surfaces (Chains / DEX /
Bridge / AI Compute nav + the `/chains`, `/chains/[slug]`, `/dex` pages) are
gated on `isPrimaryNetworkExplorer()`. Nav hides them on brand/white-label
hosts; `ui/shared/PrimaryNetworkGuard` backstops direct-URL access so the 10
VMs can never leak onto Hanzo/Zoo/Pars. Proven by
`configs/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 the
`explore-env-{brand}` configmaps (APP_HOST), not baked per-brand. Deploy = bump
all four `explore-fe-*` images to v1.0.14 in
`luxfi/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}`, initial
`event: CONNECT`, `: ping` keep-alive). Result: repeating `ws 404` →
"Live updates temporarily delayed". This is a frontend-only transport
mismatch; the chain/indexer are untouched.
- **Fix:** `lib/socket/sse.ts` reimplements the slice of the Phoenix
`Socket`/`Channel` API the 27 realtime consumers use, backed by ONE
`EventSource`. The seam is unchanged — `SocketProvider` / `useSocketChannel`
/ `useSocketMessage` and every consumer keep working; only the transport
underneath swapped. No more `phoenix` import in app code.
- **Routing:** envelope `{event, chain, data, topic?}`. With `topic` →
delivered straight to that channel/event (scoped `addresses:*`/`tokens:*`,
and the component-test harness). Without → the `TRANSLATIONS` table maps the
backend hub channel to a Blockscout `(topic, event)`; today the indexer
broadcasts only `blocks` → `blocks:new_block` / `new_block`. `join()` resolves
`ok` on stream open; EventSource auto-reconnects. URL normaliser collapses any
`wss://…/socket*` (or test `ws://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, join `ok`,
on/off, onError, disconnect. CT harness `playwright/fixtures/socketServer.ts`
is now a tiny SSE server (same `createSocket`/`joinChannel`/`sendMessage`
signatures) 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 `Block` field shapes in the `blocks` translation (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.getCurrentValidators` returns real validators (5, total weight
2.5e18 on hanzo). `pages/api/pchain.ts` derived its base URL by stripping
ONLY `/v1/chain/C/rpc` via 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.ts` was already safe (uses
`new URL().hostname`), so B/D-Chain SDK calls were unaffected.
- **"Placeholder Counter 9.074M ×8"** came from `stubs/stats.ts` `STATS_COUNTER`
(`value:'9074405', title:'Placeholder Counter'`) used as React-Query
`placeholderData` in `ui/stats/NumberWidgetsList.tsx`. When the Blockscout
**stats microservice** is absent (hanzo has NO `NEXT_PUBLIC_STATS_API_HOST`;
every `api-explore.hanzo.network/api/v1|v2/*` 404s) the stub leaked as "real".
Fix: neutralize the stub (`title:'', value:'0'`) AND gate the counters/charts
in `ui/pages/Stats.tsx` on `config.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.ts` had
hardcoded `DEMO_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.tsx` renders an honest empty/
error message per tab.
- **Bridge signers = 0 is already honest** (`lib/api/bchain/useBridgeData.ts`
returns real B-Chain signer set or `EMPTY_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.tsx` degrades to
RPC when the general indexer errors; `HOMEPAGE_STATS` stub is placeholder-only.
- **Blockchain dropdown clipped its last item** ("Verified Contracts"):
`@luxfi/ui` `Tooltip` `variant="popover"` Content defaults to
`overflow-hidden px-3 py-2`, clipping a tall menu `<ul>`. Fixed at the
consumer (`ui/snippets/navigation/horizontal/NavLinkGroup.tsx`) via
`contentProps={{ 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 of `explore-fe-*` Deployments
on do-sfo3-lux-k8s ns `lux-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 per `styles/tokens.css` (light
`rgba(16,17,18,.80)` / dark `rgba(255,255,255,.80)`), so all descendants now
render the mode-correct color (dark on light, light on dark).
- Stake unit was hardcoded `LUX` on every brand; now `config.chain.currency.
symbol || 'LUX'` (= `AI` on Hanzo) — matches `NetworkStats.tsx`'s pattern,
DRY across all 6 stake labels in the three components.
- NOT changed: `formatStake` uses `LUX_DECIMALS = 6`, the codebase-wide
convention (NetworkStats + ChainDetailPage use the same); P-chain
`getMinStake` returns `minValidatorStake: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.js` has `typescript.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 of `explore-fe-{hanzo,lux,zoo}` on
do-sfo3-lux-k8s ns `lux-mainnet`. (Built on do-sfo3-hanzo-k8s ns `hanzo`.)
### Multi-brand hostname-driven header logo (explore v1.0.10)
- ONE image white-labels by request Host: `configs/app/chainRegistry.ts`
`getCurrentChain()` resolves the chain (and its brand) from the hostname,
and the header renders `chain.branding.logoContent` (an inline-SVG mark
using `currentColor`). NO hardcoded logo, no image-URL→triangle fallback.
- Per-brand marks live in `chainRegistry.ts`: Lux = downward triangle
(viewBox `0 0 100 100`), Hanzo = real blocky-H (`@hanzo/logo`, viewBox
`0 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), and
`ui/snippets/networkLogo/{NetworkLogo,NetworkIcon}.tsx` (mobile header
`ui/snippets/header/HeaderMobile.tsx` + chain-switcher). The stale
pre-v1.0.10 mobile `NetworkIcon` hardcoded a `viewBox="0 0 50 50"`
`<polygon>` triangle mislabeled `aria-label="Hanzo icon"` — fixed in
4369780 (mobile) + 79dbe63 (desktop, single header per breakpoint).
- Multi-tenant L1/L2/L3: each `ChainEntry` has a `vm` field
(`EVM` = Lux primary L1, `L2` = brand sovereign chains) gating
`isPrimaryNetworkExplorer()` 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 pool
`runner-pool-32g`, secret `kaniko-ghcr-multi`, toleration
`dedicated=ci-runner`): context `git://github.com/luxfi/explore.git#main`,
`--dockerfile Dockerfile`, fan-out `--destination` to per-brand GHCR orgs
`ghcr.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-mainnet` as `explore-fe-<brand>`
Deployments (selector `{app:explore-fe,brand,network}`), env from
`explore-env-<brand>` ConfigMap, pull secret `ghcr-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 on `api-explore.*` (measured), so none of it could
ever have worked here.
- `lib/oidc.ts` is 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/userinfo` paths 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 shipped
`NEXT_PUBLIC_OIDC_CLIENT_ID=lux-explore-client-id`, a client that never
existed → `400 unknown client_id`. Issuers are `lux.id`, **`zoolabs.id`**
(NOT `zoo.id` — it does not resolve), `hanzo.id`, `pars.id`.
- IAM state is changed ONLY by `make iam-provision` from that org's
`universe/infra/k8s/iam/provision.yaml`. 🪤 The upsert **REPLACES**
`redirectUris` — it does not merge. Any live URI not derivable from `hosts:`
must be stated under `redirects:` or the next converge deletes it.
### UI stack — the migration is already done; Tailwind is NOT removable
- Measured: `@chakra-ui` 0 files, `@emotion` 0, `styled-system` 0, `@radix-ui`
**0 files in our code**. `@luxfi/ui` is imported by ~787 files.
- The ~17 `@radix-ui/*` entries in `package.json` have 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.css` has
`@source "../node_modules/@luxfi/ui"` precisely so Tailwind emits the
utilities `@luxfi/ui` itself uses; purge them and the library breaks. There is
no `tailwind.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/ui` absorbs via an
`any`-typed compat shim. If that shim goes, those files break.
### Deployment traps
- The four `ghcr.io/{luxfi,hanzoai,zooai,parsdao}/explore` tags are **byte-
identical copies** (same digest) — brand comes entirely from the
`explore-env-<brand>` ConfigMap at runtime. A version bump is therefore
`crane copy`, not four builds. Never mix org registries.
- 🪤 `tsconfig.json` has `incremental: true`. A stale `tsconfig.tsbuildinfo`
reports errors against file contents that no longer exist. Delete it before
trusting a typecheck diff.
- 🪤 `.cspell-words.txt` is in cspell's **ignorePaths**, not a dictionary. New
words go in the `words` array in `cspell.jsonc` or 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 `platform.getBlockchains`, 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.ts` additionally 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. `/chains` and `/chains/<slug>` join the two.
- **The public gateway routes exactly three chains: `/v1/chain/P`, `/v1/chain/X` and
`/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: false` on all three networks** and is the sole
entry in `/v1/health`'s not-bootstrapped list — which is why all three
networks report `healthy: 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.*`, not `avm.*`.** `avm` returns "can't find service".
`xvm.getBlockchainID` and `xvm.getTxFee` are 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 <tag> 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`/`*.bak` backups (~1.9 MB) left by the Blockscout→Lux
rebrand commits; two still carried raw `<<<<<<< HEAD` conflict markers. Nothing
referenced them. `*.orig`/`*.rej`/`*.bak` are 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.