fix(explore): un-stub org explorers (TLS) + mobile detail pages (v1.1.10)

Two mobile-QA defects across the block explorers.

Defect 1 (org explorers showed goerli stub data): api-explore.{zoo,hanzo,
pars}.network served the self-signed INGRESS DEFAULT CERT (no cert-manager
Ingress for those SNIs), so browsers rejected every FE fetch and react-query
fell back to the goerli stubs behind stuck skeletons. FE wiring + backend were
already correct (all 6 chains served real per-chain data via the existing
IngressRoutes). Fix: dedicated letsencrypt-prod Ingress per org api host ->
explorer-unified:8090, mirroring api-explore-lux-network. Certs issued, all
three now ssl_verify=0, real data flows. Also source per-org brand identity
(NETWORK_NAME/currency) in the configmaps so titles render the right brand.

Defect 2 (mobile 390px detail pages: dropped labels, horizontal clip, tab
overlap): three shared-UI fixes.
- Footer: was the real horizontal-overflow driver — inline gridTemplateColumns
  minmax(auto,470px) never collapsed on mobile. Now grid-cols-1 on mobile.
- DetailedInfo.ItemValue: add min-w-0 + mobile wrap so long address/hash values
  stop forcing the page wider than the viewport (desktop unchanged).
- AdaptiveTabsList: @luxfi/ui Tabs silently discard Chakra style props
  (overflowX/w/flexShrink/...), so express mobile scroll + shrink-0 as
  className. Tab strip now scrolls instead of collapsing/overlapping.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
zeekay
2026-07-18 10:31:56 -07:00
co-authored by Hanzo Dev
parent 29a75132fa
commit 26fdf9c609
7 changed files with 233 additions and 57 deletions
+55
View File
@@ -166,6 +166,61 @@ All symlinks reference this single source of truth.
## Recent Changes
### Org explorers un-stubbed + mobile detail pages fixed (v1.1.10)
Two mobile-QA defects across the block explorers.
- **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
@@ -14,6 +14,16 @@ metadata:
namespace: lux-mainnet
labels: {app: explore-fe, brand: lux, network: mainnet}
data:
# Brand identity. Intended to be baked into the image, but the current
# ghcr.io/luxfi/explore image does not set NETWORK_NAME/currency as runtime
# env, so make_envs_script.sh never writes them to window.__envs and the
# client renders "undefined stats" / undefined currency. Source them here
# (make_envs reads pod env at startup) until the image bakes them.
NEXT_PUBLIC_NETWORK_NAME: Lux Network
NEXT_PUBLIC_NETWORK_SHORT_NAME: LUX
NEXT_PUBLIC_NETWORK_CURRENCY_NAME: Lux
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: LUX
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS: "18"
NEXT_PUBLIC_NETWORK_ID: "96369"
NEXT_PUBLIC_IS_TESTNET: "false"
NEXT_PUBLIC_API_HOST: api-explore.lux.network
@@ -43,6 +53,13 @@ metadata:
namespace: lux-mainnet
labels: {app: explore-fe, brand: zoo, network: mainnet}
data:
# Brand identity (see explore-env-lux note): source here so make_envs writes
# them to window.__envs, otherwise the client shows "undefined stats".
NEXT_PUBLIC_NETWORK_NAME: Zoo Network
NEXT_PUBLIC_NETWORK_SHORT_NAME: ZOO
NEXT_PUBLIC_NETWORK_CURRENCY_NAME: Zoo
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: ZOO
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS: "18"
NEXT_PUBLIC_NETWORK_ID: "200200"
NEXT_PUBLIC_IS_TESTNET: "false"
NEXT_PUBLIC_API_HOST: api-explore.zoo.network
@@ -64,6 +81,13 @@ metadata:
namespace: lux-mainnet
labels: {app: explore-fe, brand: hanzo, network: mainnet}
data:
# Brand identity (see explore-env-lux note): source here so make_envs writes
# them to window.__envs, otherwise the client shows "undefined stats".
NEXT_PUBLIC_NETWORK_NAME: Hanzo
NEXT_PUBLIC_NETWORK_SHORT_NAME: HANZO
NEXT_PUBLIC_NETWORK_CURRENCY_NAME: Hanzo AI
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: AI
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS: "18"
NEXT_PUBLIC_NETWORK_ID: "36963"
NEXT_PUBLIC_IS_TESTNET: "false"
NEXT_PUBLIC_API_HOST: api-explore.hanzo.network
@@ -84,6 +108,13 @@ metadata:
namespace: lux-mainnet
labels: {app: explore-fe, brand: pars, network: mainnet}
data:
# Brand identity (see explore-env-lux note): source here so make_envs writes
# them to window.__envs, otherwise the client shows "undefined stats".
NEXT_PUBLIC_NETWORK_NAME: Pars
NEXT_PUBLIC_NETWORK_SHORT_NAME: PARS
NEXT_PUBLIC_NETWORK_CURRENCY_NAME: Pars
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: PARS
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS: "18"
NEXT_PUBLIC_NETWORK_ID: "494949"
NEXT_PUBLIC_IS_TESTNET: "false"
NEXT_PUBLIC_API_HOST: api-explore.pars.network
@@ -39,3 +39,105 @@ spec:
- hosts:
- api-explore.lux.network
secretName: api-explore-lux-network-tls
---
# Zoo Mainnet — explorer BACKEND API ingress (api-explore.zoo.network).
#
# Same root cause as lux above: without a dedicated Ingress declaring a TLS
# block for this SNI, the controller serves the self-signed "INGRESS DEFAULT
# CERT", browsers reject every api-explore.zoo.network request
# (ERR_CERT_AUTHORITY_INVALID), the FE data fetches fail, and react-query
# falls back to the goerli stub placeholders behind stuck skeleton loaders.
# The chain routing already works (the IngressRoute lux-explorer-zoo rewrites
# to /v1/indexer/zoo via the blockscout-prefix-zoo middleware and returns real
# Zoo 200200 data) — this Ingress only supplies the browser-valid LE cert.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-explore-zoo-network
namespace: lux-mainnet
labels: {app: explore-fe, brand: zoo, network: mainnet}
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
spec:
ingressClassName: ingress
rules:
- host: api-explore.zoo.network
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: explorer-unified
port:
number: 8090
tls:
- hosts:
- api-explore.zoo.network
secretName: api-explore-zoo-network-tls
---
# Hanzo Mainnet — explorer BACKEND API ingress (api-explore.hanzo.network).
# See the zoo block above; routes real Hanzo 36963 data via the
# lux-explorer-hanzo IngressRoute + blockscout-prefix-hanzo. This Ingress only
# supplies the browser-valid LE cert for the api-explore.hanzo.network SNI.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-explore-hanzo-network
namespace: lux-mainnet
labels: {app: explore-fe, brand: hanzo, network: mainnet}
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
spec:
ingressClassName: ingress
rules:
- host: api-explore.hanzo.network
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: explorer-unified
port:
number: 8090
tls:
- hosts:
- api-explore.hanzo.network
secretName: api-explore-hanzo-network-tls
---
# Pars Mainnet — explorer BACKEND API ingress (api-explore.pars.network).
# See the zoo block above; routes real Pars 494949 data via the
# lux-explorer-pars IngressRoute + blockscout-prefix-pars. This Ingress only
# supplies the browser-valid LE cert for the api-explore.pars.network SNI.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-explore-pars-network
namespace: lux-mainnet
labels: {app: explore-fe, brand: pars, network: mainnet}
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
spec:
ingressClassName: ingress
rules:
- host: api-explore.pars.network
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: explorer-unified
port:
number: 8090
tls:
- hosts:
- api-explore.pars.network
secretName: api-explore-pars-network-tls
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@luxfi/explore",
"version": "1.1.9",
"version": "1.1.10",
"private": false,
"homepage": "https://github.com/luxfi/explore#readme",
"engines": {
@@ -1,12 +1,13 @@
import { Skeleton } from '@luxfi/ui/skeleton';
import type { TabsProps } from '@luxfi/ui/tabs';
import { TabsCounter, TabsList, TabsTrigger } from '@luxfi/ui/tabs';
import React from 'react';
import type { TabItemRegular } from './types';
import useIsMobile from 'lib/hooks/useIsMobile';
import { cn } from 'lib/utils/cn';
import { Skeleton } from '@luxfi/ui/skeleton';
import type { TabsProps } from '@luxfi/ui/tabs';
import { TabsCounter, TabsList, TabsTrigger } from '@luxfi/ui/tabs';
import { useIsSticky } from '../../hooks/useIsSticky';
import AdaptiveTabsMenu from './AdaptiveTabsMenu';
import useAdaptiveTabs from './useAdaptiveTabs';
@@ -94,26 +95,18 @@ const AdaptiveTabsList = (props: Props) => {
return (
<TabsList
ref={ listRef }
flexWrap="nowrap"
alignItems="center"
whiteSpace="nowrap"
bgColor="bg.primary"
marginBottom={ 6 }
mx={{ base: '-12px', lg: 'unset' }}
px={{ base: '12px', lg: 'unset' }}
w={{ base: 'calc(100% + 24px)', lg: '100%' }}
overflowX={{ base: 'auto', lg: 'initial' }}
overscrollBehaviorX="contain"
css={{
scrollSnapType: 'x mandatory',
scrollPaddingInline: '12px', // mobile page padding
// hide scrollbar
'&::-webkit-scrollbar': { /* Chromiums */
display: 'none',
},
'-ms-overflow-style': 'none', /* IE and Edge */
scrollbarWidth: 'none', /* Firefox */
}}
// @luxfi/ui TabsList discards Chakra style props (overflowX/w/mx/px/css…),
// so the mobile tab-bar behavior must be expressed as Tailwind classes —
// the only styling hook it forwards. Without this the bar can't scroll on
// mobile and the (non-shrinking) triggers collide past the 390px edge.
className={ cn(
'flex-nowrap items-center whitespace-nowrap bg-[var(--color-bg-primary)] mb-6',
// mobile: bleed to the page edges, horizontal scroll-snap, hidden scrollbar
'-mx-3 px-3 w-[calc(100%+24px)] overflow-x-auto overscroll-x-contain',
'snap-x snap-mandatory scroll-px-3 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden',
// desktop: no bleed, no scroll — the adaptive "…" menu handles overflow
'lg:mx-0 lg:px-0 lg:w-full lg:overflow-x-visible lg:snap-none',
) }
{
...(props.stickyEnabled ? {
position: 'sticky',
@@ -162,8 +155,7 @@ const AdaptiveTabsList = (props: Props) => {
key={ value }
value={ value }
ref={ ref }
scrollSnapAlign="start"
flexShrink={ 0 }
className="shrink-0 snap-start"
{ ...getItemStyles(index, tabsCut, isLoading) }
>
{ typeof tab.title === 'function' ? tab.title() : tab.title }
@@ -177,12 +169,12 @@ const AdaptiveTabsList = (props: Props) => {
<TabsTrigger
key={ value }
value={ value }
flexShrink={ 0 }
bgColor={
className={ cn(
'shrink-0 snap-start',
activeTabIndex === index && (variant === 'solid' || variant === undefined) ?
{ _light: 'blackAlpha.50', _dark: 'whiteAlpha.50' } :
undefined
}
'bg-black/5 dark:bg-white/5' :
undefined,
) }
>
<Skeleton loading>
{ typeof tab.title === 'function' ? tab.title() : tab.title }
+8 -1
View File
@@ -70,7 +70,14 @@ export const ItemValue = ({ children, multiRow = false, className }: ItemValuePr
return (
<div
className={ cn(
'flex items-center pl-6 lg:pl-0 min-h-[30px] lg:min-h-[32px] whitespace-nowrap',
// Mobile: keep a long value (address/hash entity) from pushing the page
// wider than the viewport. min-w-0 lets the flex child shrink so the
// entity's dynamic shortener truncates to fit the cell; break-words wraps
// plain long strings; overflow-x-hidden is the hard guarantee that the
// cell can never overflow past its column edge. Desktop is unchanged
// (single-line nowrap, visible overflow).
'flex items-center pl-6 lg:pl-0 min-h-[30px] lg:min-h-[32px]',
'min-w-0 overflow-x-hidden whitespace-normal break-words lg:overflow-x-visible lg:whitespace-nowrap',
multiRow && 'flex-wrap leading-[30px] lg:leading-[32px]',
className,
) }
+14 -25
View File
@@ -1,3 +1,4 @@
import { Skeleton } from '@luxfi/ui/skeleton';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
@@ -9,7 +10,6 @@ import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import useFetch from 'lib/hooks/useFetch';
import { Link } from 'toolkit/next/link';
import { Skeleton } from '@luxfi/ui/skeleton';
import { copy } from 'toolkit/utils/htmlEntities';
import { CONTENT_MAX_WIDTH } from 'ui/shared/layout/utils';
import NetworkAddToWallet from 'ui/shared/NetworkAddToWallet';
@@ -149,29 +149,28 @@ const Footer = () => {
};
const horizontalPadding = config.UI.navigation.layout === 'horizontal' ? 'lg:px-6' : 'lg:px-12';
// mobile: single column so the network description + link groups wrap inside
// the viewport instead of forcing a 470px column past the 390px edge.
const footerGridCols = 'grid grid-cols-1 lg:grid-cols-[minmax(auto,470px)_1fr]';
if (config.UI.footer.links) {
return (
<footer className="border-t border-[var(--color-border-divider)]">
<div
className={ `grid px-4 ${ horizontalPadding } 2xl:px-6 py-4 lg:py-8 mx-auto gap-x-8 lg:gap-x-[100px]` }
style={{
maxWidth: `${ CONTENT_MAX_WIDTH }px`,
gridTemplateColumns: 'minmax(auto, 470px) 1fr',
}}
className={ `${ footerGridCols } px-4 ${ horizontalPadding } 2xl:px-6 py-4 lg:py-8 mx-auto gap-x-8 lg:gap-x-[100px]` }
style={{ maxWidth: `${ CONTENT_MAX_WIDTH }px` }}
>
<div>
<div className="min-w-0">
{ renderNetworkInfo() }
{ renderProjectInfo() }
{ renderRecaptcha() }
</div>
<div
className="grid gap-2 lg:gap-8 xl:gap-12 mt-8 lg:mt-0"
style={{
gridTemplateColumns: `repeat(${ colNum }, 160px)`,
justifyContent: 'flex-end',
}}
// mobile: 2 flexible columns that fit the viewport; desktop: the
// measured number of fixed 160px columns, right-aligned.
className="grid grid-cols-2 lg:grid-cols-[repeat(var(--footer-cols),160px)] justify-start lg:justify-end gap-2 lg:gap-8 xl:gap-12 mt-8 lg:mt-0"
style={{ '--footer-cols': colNum } as React.CSSProperties}
>
{
([
@@ -197,25 +196,15 @@ const Footer = () => {
return (
<footer className="border-t border-[var(--color-border-divider)]">
<div
className={ `grid px-4 ${ horizontalPadding } 2xl:px-6 py-4 lg:py-8 mx-auto` }
style={{
maxWidth: `${ CONTENT_MAX_WIDTH }px`,
gridTemplateColumns: 'minmax(auto, 470px) 1fr',
columnGap: '100px',
}}
className={ `${ footerGridCols } lg:gap-x-[100px] px-4 ${ horizontalPadding } 2xl:px-6 py-4 lg:py-8 mx-auto` }
style={{ maxWidth: `${ CONTENT_MAX_WIDTH }px` }}
>
{ renderNetworkInfo() }
{ renderProjectInfo() }
{ renderRecaptcha() }
<div
className="grid gap-1 mt-8 lg:mt-0"
style={{
gridTemplateColumns: 'repeat(auto-fill, 160px)',
gridAutoFlow: 'column',
alignContent: 'start',
justifyContent: 'flex-end',
}}
className="grid grid-cols-2 lg:grid-flow-col lg:auto-cols-[160px] content-start justify-start lg:justify-end gap-1 mt-8 lg:mt-0"
>
{ FOOTER_LINKS.map(link => <FooterLinkItem { ...link } key={ link.text }/>) }
</div>