From 0448d9df7a1ac6ddff1e1a2912bbdf2049e1195c Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Wed, 16 Sep 2026 09:14:22 +0300 Subject: [PATCH] fix: say what is happening while connecting, and stop defaulting to a light client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The deployed page was a bare black screen for minutes with nothing but the tab spinner to suggest it was not simply broken. Two separate faults. **No fallback.** `main.tsx` wraps the whole app in ``, and Subscribe's `fallback` defaults to null — "it will render null until the subscription exists". `dynamicBuilder$` does not emit until a chain has been followed and its metadata decoded, so until then the document body is empty. On a slow connection that is seconds of nothing; on a chain the console cannot read it is forever, and a visitor cannot tell the two apart. There is now a splash naming the chain and the endpoint, which escalates at 8s and again at 25s and tells the reader how to force a different network. **The default endpoint was the light client.** `defaultSelectedChain` used LIGHT_CLIENT_ENDPOINT unconditionally, which routes through smoldot — and smoldot needs a chain spec. The Quantus networks have none (`hasChainSpecs: false`), so the default asked it to follow a chain it had never heard of. It now picks AUTO_RPC for any network without chain specs, which is what the endpoints in quantus.json are for. This does not make the console work against Quantus. It makes it explain itself while failing, which it could not do before — the remaining fault is much deeper and is written up on #1. Refs #1, #3 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f --- src/components/BootSplash.tsx | 81 +++++++++++++++++++++++++++++++++ src/main.tsx | 19 +++++++- src/state/chains/chain.state.ts | 11 ++++- 3 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 src/components/BootSplash.tsx diff --git a/src/components/BootSplash.tsx b/src/components/BootSplash.tsx new file mode 100644 index 0000000..3048f81 --- /dev/null +++ b/src/components/BootSplash.tsx @@ -0,0 +1,81 @@ +import { FC, useEffect, useState } from "react" + +/** + * What the page shows while the chain is being reached. + * + * `main.tsx` wraps the entire app in ``, + * and `Subscribe`'s fallback defaults to `null` — "it will render null until the + * subscription exists". `dynamicBuilder$` does not emit until a chain has been + * followed and its metadata decoded, so until then the document body is empty + * and a visitor sees a bare black page with nothing but the tab spinner to + * suggest it is not simply broken. + * + * That is indistinguishable from a crash, and on a chain the console cannot read + * it never resolves at all. So: say what is happening, and say it louder the + * longer it takes. + */ + +const SLOW_MS = 8_000 +const STUCK_MS = 25_000 + +export const BootSplash: FC<{ endpoint?: string; network?: string }> = ({ + endpoint, + network, +}) => { + const [elapsed, setElapsed] = useState(0) + + useEffect(() => { + const started = Date.now() + const id = setInterval(() => setElapsed(Date.now() - started), 500) + + return () => clearInterval(id) + }, []) + + return ( +
+
+ qapi{" "} + console +
+ +
+ +
+ Connecting to {network ?? "the chain"} + {endpoint ? ( + <> + {" "} + at {endpoint} + + ) : null} + … +
+ + {elapsed > SLOW_MS && elapsed <= STUCK_MS ? ( +
+ Still going. The console downloads the chain's metadata and decodes it + before it can render anything, which takes a moment on a first visit. +
+ ) : null} + + {elapsed > STUCK_MS ? ( +
+

+ This is taking much longer than it should. +

+

+ Either the node is unreachable, or the console cannot read this + chain. Check the browser console for the reason, and try another + network — the switcher is in the header once any chain loads, and{" "} + #networkId=polkadot&endpoint=wss://rpc.polkadot.io{" "} + in the URL will force one. +

+
+ ) : null} +
+ ) +} diff --git a/src/main.tsx b/src/main.tsx index be99382..0561f70 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,4 +1,4 @@ -import { dynamicBuilder$ } from "@/state/chains/chain.state" +import { dynamicBuilder$, getDefaultChain } from "@/state/chains/chain.state" import { RemoveSubscribe, Subscribe } from "@react-rxjs/core" import { MultisigExternalSignerModal, PolkaHubProvider } from "polkahub" import { StrictMode } from "react" @@ -8,6 +8,7 @@ import { merge } from "rxjs" import App from "./App.tsx" import { TooltipProvider } from "./components/ui/tooltip.tsx" import "./index.css" +import { BootSplash } from "./components/BootSplash.tsx" import { codeSplit$ } from "./lib/externalState.ts" import { explorer$ } from "./pages/Explorer" import { polkaHub } from "./state/polkahub.ts" @@ -17,8 +18,22 @@ const metrics$ = codeSplit$(() => import("./pages/Metrics/metrics.state.ts").then((r) => r.blockStats$), ) +// Without a fallback, `Subscribe` renders null until `dynamicBuilder$` emits — +// which is after a chain has been followed and its metadata decoded. On a slow +// connection that is seconds of empty black page; on a chain the console cannot +// read, it is forever, and indistinguishable from a crash. +const initial = getDefaultChain() + createRoot(document.getElementById("root")!).render( - + + } + source$={merge(dynamicBuilder$, explorer$, metrics$)} + > diff --git a/src/state/chains/chain.state.ts b/src/state/chains/chain.state.ts index 47c5379..af0ffb0 100644 --- a/src/state/chains/chain.state.ts +++ b/src/state/chains/chain.state.ts @@ -145,10 +145,17 @@ export const isValidUri = (input: string): boolean => { const defaultSelectedChain: SelectedChain = { network: defaultNetwork, - endpoint: LIGHT_CLIENT_ENDPOINT, + // AUTO_RPC, not LIGHT_CLIENT. A light client needs a chain spec, and the + // Quantus networks have none (`hasChainSpecs: false`), so defaulting to + // smoldot asks it to follow a chain it has never heard of — which fails + // before a single byte is fetched, and with `Subscribe`'s null fallback it + // failed as a bare black page. + endpoint: defaultNetwork.lightclient + ? LIGHT_CLIENT_ENDPOINT + : AUTO_RPC_ENDPOINT, forkMethod: "none", } -const getDefaultChain = (): SelectedChain => { +export const getDefaultChain = (): SelectedChain => { const hashParams = getHashParams() if (hashParams.has("networkId") && hashParams.has("endpoint")) { const networkId = hashParams.get("networkId")!