diff --git a/frontend/app/api.ts b/frontend/app/api.ts index effa20f..38fc669 100644 --- a/frontend/app/api.ts +++ b/frontend/app/api.ts @@ -191,11 +191,30 @@ export class ApiError extends Error { // ─── Fetch helpers ──────────────────────────────────────────────────────────── +const REQUEST_TIMEOUT_MS = 30_000; + async function request(path: string, options: RequestInit = {}): Promise { - const res = await fetch(`/api${path}`, { - headers: { "Content-Type": "application/json", ...options.headers }, - ...options, - }); + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); + + let res: Response; + try { + res = await fetch(`/api${path}`, { + headers: { "Content-Type": "application/json", ...options.headers }, + signal: controller.signal, + ...options, + }); + } catch (err) { + if (err instanceof DOMException && err.name === "AbortError") { + throw new ApiError( + 0, + `Request timed out after ${REQUEST_TIMEOUT_MS / 1000}s`, + ); + } + throw err; + } finally { + clearTimeout(timer); + } const raw = await res.text(); let body: ApiResponse | null = null; diff --git a/frontend/app/components/LoadingState.tsx b/frontend/app/components/LoadingState.tsx new file mode 100644 index 0000000..cef680d --- /dev/null +++ b/frontend/app/components/LoadingState.tsx @@ -0,0 +1,32 @@ +import { Loader2 } from "lucide-react"; + +interface LoadingStateProps { + /** Primary message, e.g. "Loading maker data" */ + message: string; + /** Optional secondary detail, e.g. "Fetching wallet balance…" */ + detail?: string; + /** Show as full-page centered (default) or inline */ + inline?: boolean; +} + +export default function LoadingState({ + message, + detail, + inline, +}: LoadingStateProps) { + const content = ( +
+ +
+

{message}

+ {detail && ( +

{detail}

+ )} +
+
+ ); + + if (inline) return content; + + return
{content}
; +} diff --git a/frontend/app/routes/home.tsx b/frontend/app/routes/home.tsx index 0c7e568..6d30195 100644 --- a/frontend/app/routes/home.tsx +++ b/frontend/app/routes/home.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { Link } from "react-router-dom"; import { X } from "lucide-react"; import Nav from "../components/Nav"; +import LoadingState from "../components/LoadingState"; import OnboardingWizard from "./onboarding"; import { makers, @@ -44,6 +45,8 @@ function swapKey( export default function Home() { const [makerRows, setMakerRows] = useState([]); const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [loadingDetail, setLoadingDetail] = useState(""); const [error, setError] = useState(null); const [pending, setPending] = useState>(new Set()); const swapHistoryCache = useRef>({}); @@ -51,13 +54,26 @@ export default function Home() { const lastSwapRefreshAt = useRef(0); async function loadMakers(forceSwapRefresh = false) { + const isInitialLoad = useRef(true); + + const initial = isInitialLoad.current; + if (!initial) setRefreshing(true); try { setError(null); + if (initial) setLoadingDetail("Fetching maker list…"); const list = await makers.list(); + if (initial) + setLoadingDetail( + `Loading details for ${list.length} maker${list.length !== 1 ? "s" : ""}…`, + ); const includeSwaps = forceSwapRefresh || lastSwapRefreshAt.current === 0 || Date.now() - lastSwapRefreshAt.current >= SWAP_HISTORY_REFRESH_MS; + if (initial) + setLoadingDetail( + `Loading details for ${list.length} maker${list.length !== 1 ? "s" : ""}…`, + ); const rows = await Promise.all( list.map(async ({ id }): Promise => { const requests = [ @@ -120,6 +136,8 @@ export default function Home() { setError(err instanceof Error ? err.message : "Failed to load makers"); } finally { setLoading(false); + setRefreshing(false); + isInitialLoad.current = false; } } @@ -152,9 +170,10 @@ export default function Home() { return (
); } @@ -253,12 +272,20 @@ export default function Home() {

Your Makers

- - + Add New Maker - +
+ {refreshing && ( + + + Refreshing... + + )} + + + Add New Maker + +
diff --git a/frontend/app/routes/makerDetails/index.tsx b/frontend/app/routes/makerDetails/index.tsx index 54af5c2..b22f7b7 100644 --- a/frontend/app/routes/makerDetails/index.tsx +++ b/frontend/app/routes/makerDetails/index.tsx @@ -1,7 +1,8 @@ -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useRef } from "react"; import { useParams } from "react-router-dom"; import { Zap, Moon } from "lucide-react"; import Nav from "../../components/Nav"; +import LoadingState from "../../components/LoadingState"; import { makers, wallet, @@ -41,17 +42,21 @@ export default function MakerDetails() { const [torAddress, setTorAddress] = useState(null); const [dataDir, setDataDir] = useState(null); const [loading, setLoading] = useState(true); + const [loadingDetail, setLoadingDetail] = useState(""); const [error, setError] = useState(null); const [actionLoading, setActionLoading] = useState(false); const [syncLoading, setSyncLoading] = useState(false); const [syncMsg, setSyncMsg] = useState(null); + const isInitialLoad = useRef(true); const [walletRefreshToken, setWalletRefreshToken] = useState(0); const loadCore = useCallback(async () => { if (!id) return; - setLoading(true); + const initial = isInitialLoad.current; + if (initial) setLoading(true); setError(null); try { + if (initial) setLoadingDetail("Fetching maker config and status…"); const [infoData, statusData, balanceData, reportsData] = await Promise.allSettled([ makers.get(id), @@ -75,6 +80,7 @@ export default function MakerDetails() { setEarningsSats(0); } + if (initial) setLoadingDetail("Resolving Tor address…"); monitoring .torAddress(id) .then(setTorAddress) @@ -87,6 +93,7 @@ export default function MakerDetails() { setError(e instanceof Error ? e.message : "Failed to load maker data"); } finally { setLoading(false); + isInitialLoad.current = false; } }, [id]); @@ -137,6 +144,19 @@ export default function MakerDetails() { }; const swapLiquidity = balances ? `${satsToBtc(balances.swap)} BTC` : "—"; + + if (loading) { + return ( +
+
+ ); + } + return (
diff --git a/frontend/app/routes/makerDetails/wallet.tsx b/frontend/app/routes/makerDetails/wallet.tsx index e1e183c..4a2e5e4 100644 --- a/frontend/app/routes/makerDetails/wallet.tsx +++ b/frontend/app/routes/makerDetails/wallet.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useCallback } from "react"; +import { Loader2 } from "lucide-react"; import { wallet, satsToBtc, btcToSats, type UtxoInfo } from "../../api"; interface Props { @@ -232,10 +233,16 @@ export default function Wallet({ id, onBalanceRefresh, refreshToken }: Props) { )} {utxosLoading ? ( -
- {[...Array(3)].map((_, i) => ( -
- ))} +
+
+ + Fetching UTXOs from wallet… +
+
+ {[...Array(3)].map((_, i) => ( +
+ ))} +
) : utxos && utxos.length > 0 ? (
diff --git a/frontend/app/routes/makersetup.tsx b/frontend/app/routes/makersetup.tsx index e3e3f11..d02d395 100644 --- a/frontend/app/routes/makersetup.tsx +++ b/frontend/app/routes/makersetup.tsx @@ -65,6 +65,31 @@ function fundsDetected(logs: string[]): boolean { l.includes("Coinselection"), ); } +/** Pattern → human-readable progress mappings */ +const progressPatterns: [string, string][] = [ + ["maker server listening on port", "Maker server started"], + ["Successfully created fidelity bond", "Fidelity bond confirmed"], + ["No spendable UTXOs available", "No funds yet — waiting for deposit"], + [ + "Insufficient fund to create fidelity bond", + "Insufficient funds for bond — waiting for deposit", + ], + ["Next sync in", "Waiting for next wallet sync cycle…"], + ["Synced & Saved", "Wallet synced and saved"], + ["Scanning completed", "Blockchain scan completed"], + ["Re-scanning Blockchain", "Re-scanning blockchain…"], + ["Sync Started for", "Syncing wallet with Bitcoin Core…"], + ["Sync at:----setup_fidelity_bond----", "Setting up fidelity bond…"], + ["Fidelity timelock", "Configuring fidelity bond timelock…"], + ["Fidelity value chosen", "Fidelity bond amount selected"], + ["No active Fidelity Bonds found", "No fidelity bonds found — creating one"], + ["Generated new Tor Hidden Service", "Generated new Tor hidden service"], + ["Generated existing Tor Hidden Service", "Tor hidden service restored"], + ["Starting Maker Server", "Starting maker server…"], + ["Selected 1 regular UTXOs", "Funds found — selecting UTXOs for bond"], + ["Coinselection", "Selecting coins for bond transaction…"], + ["Transaction seen in mempool", "Incoming transaction detected"], +]; const LOG_LEVEL_RE = /(?:^|\s|\[)(INFO|WARN|ERROR|DEBUG|TRACE)(?=$|\s|:|\])/i; @@ -78,6 +103,16 @@ function isInfoLog(line: string): boolean { return levelMatch[1].toUpperCase() === "INFO"; } +/** Extract the latest meaningful progress detail from logs for user display */ +function latestProgressDetail(logs: string[]): string | null { + for (const line of [...logs].reverse()) { + for (const [needle, detail] of progressPatterns) { + if (line.includes(needle)) return detail; + } + } + return null; +} + function inferStage( logs: string[], fidelityConfirmations: number[], @@ -104,6 +139,7 @@ export default function MakerSetup() { const [minAmount, setMinAmount] = useState(null); const [copied, setCopied] = useState(false); const [errorMsg, setErrorMsg] = useState(null); + const [progressDetail, setProgressDetail] = useState(null); const logsEndRef = useRef(null); const pollRef = useRef | null>(null); @@ -320,7 +356,8 @@ export default function MakerSetup() {
), title: "Starting Maker", - subtitle: "Initializing wallet and connecting to Bitcoin Core…", + subtitle: + progressDetail ?? "Initializing wallet and connecting to Bitcoin Core…", color: "orange", }, awaiting_funds: { @@ -330,7 +367,9 @@ export default function MakerSetup() {
), title: "Deposit Required", - subtitle: "Send Bitcoin to this address to create your fidelity bond", + subtitle: + progressDetail ?? + "Send Bitcoin to this address to create your fidelity bond", color: "orange", }, creating_bond: { @@ -358,7 +397,9 @@ export default function MakerSetup() {
), title: "Creating Fidelity Bond", - subtitle: "Funds detected — waiting for confirmation and bond creation…", + subtitle: + progressDetail ?? + "Funds detected — waiting for confirmation and bond creation…", color: "blue", }, live: { @@ -612,8 +653,8 @@ export default function MakerSetup() { d="M4 12a8 8 0 018-8v8z" /> - Watching for incoming funds — this page will update - automatically + {progressDetail ?? + "Watching for incoming funds — this page will update automatically"}
)} @@ -659,7 +700,7 @@ export default function MakerSetup() { d="M4 12a8 8 0 018-8v8z" /> - Waiting for block confirmation… + {progressDetail ?? "Waiting for block confirmation…"}
)}