Build the same source four ways from one repo:
- ghcr.io/luxfi/explore (lux brand, default)
- ghcr.io/luxfi/explore-hanzo (hanzo brand)
- ghcr.io/luxfi/explore-zoo (zoo brand)
- ghcr.io/luxfi/explore-pars (pars brand)
Each is built for linux/amd64 + linux/arm64 in parallel on hanzo ARC
runners (no QEMU), then merged into a multi-arch manifest.
Brand selection at build time via NEXT_PUBLIC_BRAND, with all
brand-specific assets (logo, favicon, theme, IAM URL) loaded from
NEXT_PUBLIC_* env vars — no hardcoded brand strings in source.
External operators set NEXT_PUBLIC_BRAND=other and supply their own
NEXT_PUBLIC_* env vars (see .env.example.external). The repo carries
no third-party trademarks.
Files:
- lib/white-label.ts brand detection + helpers
- .env.example.{lux,hanzo,zoo,pars,external} deploy templates
- .github/workflows/build-lux.yml 4-brand × 2-arch matrix
- Dockerfile accept NEXT_PUBLIC_BRAND
- configs/app/chainRegistry.ts add SPC + Pars devnet,
chainId fields, white-label
chain builder
Remove Network chain entries (mainnet/testnet/devnet/localnet)
and LIQUIDITY_BRANDING constant from chainRegistry.ts. Lux explorer must
not register / hostnames. Replace "Liquid EVM Explorer"
top-bar tagline with "Lux Explorer".
Consolidating to single canonical ARC fleet. Killing duplicate lux-build
scale set. Build job → hanzo-build-linux-amd64 (max 30 on DO). Deploy
job → hanzo-deploy-linux-amd64 (max 20 on DO, correct runner class for
kubectl deploys).
No behavior change for this workflow.
Co-authored-by: Hanzo Dev <[email protected]>
The existing Next.js app (99 pages, 427 next/* imports, 177 files using
next/router, 154 using nextjs-routes types, 105 using getServerSideProps) is
far too coupled to rewrite in one pass. A full migration is ~2–4 weeks of
mechanical work.
This commit adds the scaffold only:
vite/package.json — Vite 8 + React 19.2 + wouter, no Next deps
vite/vite.config.ts — aliases at ../lib, ../ui, ../configs, … so source
migrates file-by-file instead of moving
vite/tsconfig.json — mirrors the aliases for TypeScript
vite/index.html — security headers as <meta> (was nextjs/headers.js)
vite/src/main.tsx — React 19 root bootstrap
vite/src/env.ts — runtime envs: window.__envs first, import.meta.env
fallback, maps NEXT_PUBLIC_API_HOST → VITE_API_BASE_URL
vite/src/redirects.ts — full port of nextjs/redirects.js (69 client rules)
vite/src/App.tsx — wouter router with CatchAll redirect resolver
vite/Dockerfile — 2-stage → ghcr.io/hanzoai/spa
vite/LLM.md — what's done, what's left, how to finish
The production Dockerfile at ../Dockerfile still builds the Next.js app. This
scaffold is independent; nothing in the existing tree is deleted or moved.
See vite/LLM.md for the full migration plan and 9 critical TODOs before this
can replace the Next.js build.
- Switch from registry cache to type=gha (no network overhead for DinD runners)
- Add concurrency group to cancel in-progress builds on same branch
- Merge build+deploy into single job (saves job startup overhead)
- Combine 7 tool installs into single RUN layer
- Add BuildKit cache mounts for pnpm store and Next.js .next/cache
- Combine sequential RUN commands in all 3 Dockerfile stages
- Disable Next.js telemetry during build and runtime
- Expand .dockerignore to exclude tests, docs, .github
- Fix token exchange endpoint to /api/login/oauth/access_token (IAM convention)
- Fix authorize URL to /login/oauth/authorize (consistent across components)
- Use lux-app-client-id (registered in IAM) instead of unregistered 'explore'
- Fetch user profile directly from IAM /api/userinfo endpoint
- Skip Blockscout auth_logout API call for OIDC provider
- Hide "scanning new transactions" warning (looks like error on low-traffic chains)
- Fix type error in useRedirectForInvalidAuthToken for OIDC error shape
envs.js is generated at container startup from environment variables.
Add Cache-Control: no-store header to ensure CDN/browser never serves
a stale version when config changes.
- Add Sign in button to TopBar with OIDC flow to lux.id
- Merge settings (color theme, identicon, address format) into user menu popover
- Remove duplicate user profile from HeroBanner
- Full-width stacked blocks/transactions layout on homepage
- Fix P-chain stats: CSP connect-src allows base origin for SDK calls
- Fix connected count: infer from uptime when field missing
- Fix dark mode popover background
- Enable OIDC in .env.production (lux.id, client: explore)
- Fixed TopBar.tsx: replaced chakra.svg with native svg element
- Fixed IconSvg.tsx: refactored to use native svg instead of chakra-wrapped SVG
- Changed Props interface to extend React.SVGAttributes instead of HTMLChakraProps
- Ensures SVG elements receive appropriate HTML attributes only
PascalCase enum values (PendingOutbound, OutboundMined, etc.) instead
of SCREAMING_SNAKE_CASE (PENDING_OUTBOUND, OUTBOUND_MINED, etc.) to
match the actual @luxfi/zetachain-cctx-types package exports.
- Rename all @blockscout/* npm packages to @luxfi/* aliases (150+ files)
- Override blue accent colors to gray for monochrome theme
- Fix dropdown/menu/popover fonts to use Geist Sans
- Change Latest Blocks to horizontal scrolling layout
- Show dashes instead of zeros for validator stats on error
- Fix GHA deploy step to use correct deployment name
- LatestBlocks: full-width layout instead of fixed 280px sidebar
- Stats: 4-column grid on desktop, 2-column on mobile
- NetworkOverview: fallback known L1 chains when P-chain API unreachable
- NetworkOverview: expose isError from useCurrentValidators
- P-chain hooks: add retry:2 for resilience
- UserProfile: use address_hash instead of id for display
- Replace middleware.ts with proxy.ts (unused in production Docker)
- Add toolkit/chakra/separator re-export
- tsconfig: jsx react-jsx, formatting cleanup
- dev.sh: disable turbopack (stability)
- P-chain API returns weight not stakeAmount; fallback to weight when stakeAmount absent
- uptime from API is already 0-100 percentage, not 0-1 fraction; remove double-multiply
- mark connected and stakeAmount as optional to match actual API response shape
Drop arm64 + manifest jobs (cluster is amd64-only). All jobs now run
on lux-build self-hosted runners — no GitHub-hosted runners needed.
Build -> deploy in 2 jobs, no intermediate manifest step.
Switches build-amd64, create-manifest, and deploy jobs from
ubuntu-latest to our native lux-build ARC runners on lux-k8s.
Eliminates QEMU emulation overhead for amd64 targets.
- getHostname() reads NEXT_PUBLIC_APP_HOST via getEnvValue during SSR
so each explorer instance shows its own branding from first paint
- _document.tsx generates dynamic SVG favicon from chain branding
- Lux: triangle from ~/work/lux/logo/
- Zoo: three overlapping circles from ~/work/zoo/logo/
- Hanzo: geometric H from ~/work/hanzo/logo/
- SPC: unicorn emoji per user request
- Pars: 8-pointed star from ~/work/pars/logo/
- Changed branding to multi-path SVGs (logoViewBox + logoContent)
- Add ChainBranding interface with logoSvg, faviconSvg, brandName per chain
- Each chain (Lux, Zoo, Hanzo, SPC, Pars) gets distinct icon + brand name
- TopBar renders chain-specific logo and brand name from registry
- ChainSwitcher shows per-chain icons in dropdown items
- Dynamic favicon via useChainFavicon hook (swaps SVG favicon on mount)
- Page title uses chain branding via getNetworkTitle()
All favicon files (PNG 16/32/48, ICO, SVG, apple-touch-icon, android-chrome)
now show the Lux triangle logo (black circle, white triangle) instead of the
Blockscout blue "h" logo.
- Logo shows "Lux Network" for C-Chain, chain name for subnets
- Blockchain items (Blocks, Txns, Tokens, Accounts, Contracts)
grouped under a dropdown to reduce nav clutter
- Network selector: minimal pill with green dot, no outline button
- Chain switcher: compact bordered pill with chevron
- Smaller font sizes (xs), tighter spacing, rounded menus
- Consistent hover states across all interactive elements
- Add centralized chain accessor module (lib/api/luxnet/chains.ts) exposing
all 14 chain VMs (C/X/P/A/B/D/G/I/K/O/Q/R/T/Z) plus utility APIs
- Refactor pchain/client.ts to re-export from centralized module
- Wire DEX page to use DexVMAPI (getStats, getPools, getMarkets) with
demo data fallback when D-Chain RPC is unavailable
- Add B-Chain bridge API module (lib/api/bchain/) using BridgeVMAPI
(getSignerSetInfo, getWaitlist) for signer set stats
- Update Bridge page to show live bridge signer count from B-Chain
- Fix stale E2E tests: use >= minimum thresholds instead of exact
counts (block/tx counts grow over time), use describe blocks
One image serves all chains — ChainSwitcher and NetworkSelector
auto-detect current chain/network from window.location.hostname.
Chain registry at configs/app/chainRegistry.ts defines all chains
across mainnet/testnet/devnet.
- Redesign TopBar as single unified header: logo + nav + controls
- Add NetworkSelector dropdown (Mainnet/Testnet/Devnet)
- Monochrome ChainSwitcher (kill blue panel)
- Connect wallet button via UserProfileDesktop in header
- Remove NavBar and SideBar (nav now in TopBar)
- Simplify HeaderDesktop to search bar only
- Network Overview homepage with P-chain stats and chain cards
- Chain detail page with validator info and indexer stats
- Chain switcher dropdown in top bar
- A-chain AI client with indexer API integration
- D-chain DEX data hook with live indexer fallback
- E2E tests for all 5 live explorer instances
- Test transaction sender script
- Remove verbose comments, section dividers, and console.log scaffolding
- A-Chain (AI): dashboard with models, providers, attestations tabs
- D-Chain (DEX): markets, orderbook, trades, pools tabs
- API hooks with mock data (ready for indexer integration)
- Navigation items and route registration for /ai and /dex
Add D-Chain (DexVM), G-Chain (GraphVM), I-Chain (IdentityVM),
O-Chain (OracleVM), R-Chain (RelayVM) to match ~/work/lux/node
vms_allvms.go. Fix VM names: ThresholdVM, BridgeVM, KeyVM.
Update chain count from 9 to 14 across all pages.
- Redesign NetworkOverview homepage with Blockscout Stats, LatestBlocks,
and LatestTxs alongside P-chain network stats and chain cards
- Add ChainSwitcher dropdown to TopBar for switching between C-Chain,
Zoo, Hanzo, SPC, and Pars explorer instances
- Create /chains/[slug] chain detail page with chain info, validators,
subnet chains, and stats (modeled after Avalanche L1 Details)
- Make ChainsPage rows clickable with links to chain detail pages
- Register /chains/[slug] route in all metadata/analytics files
- Add /bridge page with cross-chain transfer route tracker
- Show bridge routes between Primary Network chains and L1 subnets
- Add Network Stats section (validators, chains, stake, uptime) to Stats page
- Add Bridge to navigation menu
- Register /bridge route in nextjs-routes, metadata, and mixpanel
Replace slow single-platform build with parallel native builds:
- amd64 on ubuntu-latest, arm64 on ubuntu-24.04-arm
- Registry-based caching per platform
- Multi-platform manifest merge
- Auto-deploy to DOKS on push to main
Move settings from standalone TopBar gear icon into the user profile
dropdown. The menu always renders (for settings access) and includes
wallet connect/disconnect when blockchain interaction is enabled.
- Remove Settings component from TopBar
- Integrate SettingsColorTheme, SettingsIdentIcon, SettingsAddressFormat,
SettingsScamTokens, SettingsLocalTime into UserWalletMenuContent
- UserProfileDesktop always renders (no null fallback)
- UserWalletButton shows profile icon + "Menu" when no wallet connected
- Mobile header also always shows user menu
- Optimize Dockerfile.lux dependency layer
Google Fonts registers the font as 'Geist', not 'Geist Sans'. The CSS
font-family mismatch caused the font to never load, falling back to
system sans-serif. Also adds an SVG favicon with the triangle icon
(SVG favicons take priority over PNG/ICO in modern browsers).
- Fix TS error in safeFormats plugin (use any for dayjsClass parameter)
- Replace hardcoded "Blockscout" with config.chain.name in DeFiDropdown
- Replace hardcoded "LUX" SVG text with config.chain.name in LogoFallback
- Use config.chain.name in NetworkIcon fallback aria-label
- Delete unused lux-logo.tsx (dead code with hardcoded branding)
@reown/appkit-common's DateUtil sets the global dayjs locale to
'en-web3-modal' which lacks a formats property. The localizedFormat
plugin crashes with "Cannot read properties of undefined (reading
'replace')" when lowercase format tokens (lll, llll) are used on
a locale without formats. Add a safety plugin that ensures formats
are always present on whichever locale the dayjs instance uses.