-
Notifications
You must be signed in to change notification settings - Fork 4
fix: increase responsiveness #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -191,11 +191,30 @@ export class ApiError extends Error { | |
|
|
||
| // ─── Fetch helpers ──────────────────────────────────────────────────────────── | ||
|
|
||
| const REQUEST_TIMEOUT_MS = 30_000; | ||
|
|
||
| async function request<T>(path: string, options: RequestInit = {}): Promise<T> { | ||
| 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); | ||
| } | ||
|
Comment on lines
+194
to
+217
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf 'Shared timeout implementation:\n'
sed -n '194,217p' frontend/app/api.ts
printf '\nWrappers and affected mutating endpoints:\n'
sed -n '250,381p' frontend/app/api.ts | rg -n 'function (post|put|del)|=>\s*(post|put|del)\('Repository: citadel-tech/maker-dasboard Length of output: 1450 🏁 Script executed: wc -l frontend/app/api.tsRepository: citadel-tech/maker-dasboard Length of output: 94 🏁 Script executed: cat -n frontend/app/api.ts | head -250Repository: citadel-tech/maker-dasboard Length of output: 7443 🏁 Script executed: cat -n frontend/app/api.ts | tail -144Repository: citadel-tech/maker-dasboard Length of output: 6359 🏁 Script executed: cd frontend && npx prettier --write . && npm run buildRepository: citadel-tech/maker-dasboard Length of output: 1477 🏁 Script executed: # Check if there are any retry mechanisms, idempotency keys, or status polling patterns elsewhere in the codebase
rg -i "retry|idempotent|idempotency|polling|debounce" frontend/app --type ts --type tsx -A 2 -B 2Repository: citadel-tech/maker-dasboard Length of output: 98 🏁 Script executed: rg -i "retry|idempotent|idempotency|polling|debounce" frontend/app --type ts -A 2 -B 2Repository: citadel-tech/maker-dasboard Length of output: 1041 🏁 Script executed: # Check how mutating endpoints are called in components - look for error handling patterns
rg "wallet\.send|wallet\.sync|makers\.start|makers\.stop|makers\.delete|bitcoind\.start|bitcoind\.stop" frontend/app --type ts -B 3 -A 5Repository: citadel-tech/maker-dasboard Length of output: 6601 A blanket 30s abort makes write requests ambiguous.
Make timeouts configurable per endpoint. For long-running or non-idempotent operations, either implement idempotency tokens and client-side deduplication, or use status polling to confirm operation completion before retrying. 🤖 Prompt for AI Agents |
||
|
|
||
| const raw = await res.text(); | ||
| let body: ApiResponse<T> | null = null; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 = ( | ||
| <div className="flex flex-col items-center gap-3"> | ||
| <Loader2 className="w-6 h-6 text-orange-500 animate-spin" /> | ||
| <div className="text-center"> | ||
| <p className="text-gray-300 text-sm font-medium">{message}</p> | ||
| {detail && ( | ||
| <p className="text-gray-500 text-xs mt-1 animate-pulse">{detail}</p> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ); | ||
|
|
||
| if (inline) return content; | ||
|
|
||
| return <div className="flex items-center justify-center h-64">{content}</div>; | ||
|
Comment on lines
+17
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Expose the loader as a live status region. The staged ♿ Suggested fix const content = (
- <div className="flex flex-col items-center gap-3">
- <Loader2 className="w-6 h-6 text-orange-500 animate-spin" />
+ <div
+ className="flex flex-col items-center gap-3"
+ role="status"
+ aria-live="polite"
+ aria-atomic="true"
+ >
+ <Loader2
+ className="w-6 h-6 text-orange-500 animate-spin"
+ aria-hidden="true"
+ />
<div className="text-center">
<p className="text-gray-300 text-sm font-medium">{message}</p>
{detail && (🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| 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,20 +45,35 @@ | |
| export default function Home() { | ||
| const [makerRows, setMakerRows] = useState<MakerRow[]>([]); | ||
| const [loading, setLoading] = useState(true); | ||
| const [refreshing, setRefreshing] = useState(false); | ||
| const [loadingDetail, setLoadingDetail] = useState<string>(""); | ||
| const [error, setError] = useState<string | null>(null); | ||
| const [pending, setPending] = useState<Set<string>>(new Set()); | ||
| const swapHistoryCache = useRef<Record<string, UtxoInfo[]>>({}); | ||
| const swapReportCache = useRef<Record<string, SwapReportDto[]>>({}); | ||
| const lastSwapRefreshAt = useRef(0); | ||
|
|
||
| async function loadMakers(forceSwapRefresh = false) { | ||
| const isInitialLoad = useRef(true); | ||
|
Check failure on line 57 in frontend/app/routes/home.tsx
|
||
|
|
||
| const initial = isInitialLoad.current; | ||
| if (!initial) setRefreshing(true); | ||
|
Comment on lines
56
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
sed -n '56,60p' frontend/app/routes/home.tsxRepository: citadel-tech/maker-dasboard Length of output: 249 🏁 Script executed: #!/bin/bash
# Get the full structure of the component to understand context
head -80 frontend/app/routes/home.tsxRepository: citadel-tech/maker-dasboard Length of output: 2639 🏁 Script executed: #!/bin/bash
# Search for where loadMakers is called
rg "loadMakers" frontend/app/routes/home.tsx -n -A 2 -B 2Repository: citadel-tech/maker-dasboard Length of output: 659 🏁 Script executed: #!/bin/bash
# Verify the suggested fix location by checking the component state declarations
sed -n '42,56p' frontend/app/routes/home.tsxRepository: citadel-tech/maker-dasboard Length of output: 777 🏁 Script executed: #!/bin/bash
# Check if there are any other hook-related issues in the file
# Look for useEffect, useState, useRef, useCallback, useMemo, useContext, useReducer
rg "use[A-Z]\w+\(" frontend/app/routes/home.tsx -n | head -30Repository: citadel-tech/maker-dasboard Length of output: 288 🏁 Script executed: #!/bin/bash
# Verify the exact behavior by checking what happens with isInitialLoad
# Look at how isInitialLoad is used in loadMakers
sed -n '56,75p' frontend/app/routes/home.tsxRepository: citadel-tech/maker-dasboard Length of output: 828 🏁 Script executed: #!/bin/bash
# Check if isInitialLoad is referenced anywhere else in the file
rg "isInitialLoad" frontend/app/routes/home.tsx -nRepository: citadel-tech/maker-dasboard Length of output: 200 🏁 Script executed: #!/bin/bash
# Check context around where isInitialLoad is set to false (line 140)
sed -n '135,145p' frontend/app/routes/home.tsxRepository: citadel-tech/maker-dasboard Length of output: 327 🏁 Script executed: #!/bin/bash
# Let's verify the complete context of the useEffect and understand the lifecycle
sed -n '144,160p' frontend/app/routes/home.tsxRepository: citadel-tech/maker-dasboard Length of output: 607 🏁 Script executed: #!/bin/bash
# Final verification: ensure the entire loadMakers function is properly scoped
# and confirm the hook violation by checking ESLint or similar would flag this
wc -l frontend/app/routes/home.tsx && echo "---" && sed -n '52,142p' frontend/app/routes/home.tsx | head -50Repository: citadel-tech/maker-dasboard Length of output: 2104 Move Line 57 violates React's Rules of Hooks: Declare the ref once at component scope (after line 54) and read/update it inside 🔧 Suggested fix export default function Home() {
const [makerRows, setMakerRows] = useState<MakerRow[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [loadingDetail, setLoadingDetail] = useState<string>("");
const [error, setError] = useState<string | null>(null);
const [pending, setPending] = useState<Set<string>>(new Set());
const swapHistoryCache = useRef<Record<string, UtxoInfo[]>>({});
const swapReportCache = useRef<Record<string, SwapReportDto[]>>({});
const lastSwapRefreshAt = useRef(0);
+ const isInitialLoad = useRef(true);
async function loadMakers(forceSwapRefresh = false) {
- const isInitialLoad = useRef(true);
-
const initial = isInitialLoad.current;🧰 Tools🪛 GitHub Actions: Lint[error] 57-57: ESLint (react-hooks/rules-of-hooks): React Hook "useRef" is called in function "loadMakers" that is neither a React function component nor a custom React Hook. Function/component names must start uppercase; Hook names must start with "use". 🪛 GitHub Check: frontend[failure] 57-57: 🤖 Prompt for AI Agents |
||
| 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<MakerRow> => { | ||
| const requests = [ | ||
|
|
@@ -120,6 +136,8 @@ | |
| setError(err instanceof Error ? err.message : "Failed to load makers"); | ||
| } finally { | ||
| setLoading(false); | ||
| setRefreshing(false); | ||
| isInitialLoad.current = false; | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -152,9 +170,10 @@ | |
| return ( | ||
| <div className="min-h-screen bg-gray-950 text-gray-100"> | ||
| <Nav /> | ||
| <div className="flex items-center justify-center h-64"> | ||
| <div className="text-gray-400 animate-pulse">Loading makers…</div> | ||
| </div> | ||
| <LoadingState | ||
| message="Loading makers" | ||
| detail={loadingDetail || "Connecting to dashboard…"} | ||
| /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
@@ -253,12 +272,20 @@ | |
| <div className="mb-6 sm:mb-8"> | ||
| <div className="flex flex-col sm:flex-row sm:items-center justify-between mb-4 sm:mb-5 gap-3"> | ||
| <h2 className="text-lg sm:text-xl font-semibold">Your Makers</h2> | ||
| <Link | ||
| to="/addMaker" | ||
| className="px-4 sm:px-5 py-2 sm:py-2.5 bg-orange-600 text-white rounded-lg hover:bg-orange-700 active:scale-[0.97] transition-all duration-150 font-semibold text-sm w-full sm:w-auto text-center" | ||
| > | ||
| + Add New Maker | ||
| </Link> | ||
| <div className="flex items-center gap-3 w-full sm:w-auto"> | ||
| {refreshing && ( | ||
| <span className="text-xs text-gray-500 flex items-center gap-1.5"> | ||
| <span className="w-1.5 h-1.5 rounded-full bg-orange-500 animate-pulse" /> | ||
| Refreshing... | ||
| </span> | ||
| )} | ||
| <Link | ||
| to="/addMaker" | ||
| className="px-4 sm:px-5 py-2 sm:py-2.5 bg-orange-600 text-white rounded-lg hover:bg-orange-700 active:scale-[0.97] transition-all duration-150 font-semibold text-sm w-full sm:w-auto text-center" | ||
| > | ||
| + Add New Maker | ||
| </Link> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 sm:gap-5"> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,6 +65,31 @@ | |
| 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 @@ | |
| 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; | ||
| } | ||
|
Comment on lines
+106
to
+114
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
💡 Minimal wiringsetProgressDetail(latestProgressDetail(existingLogs));
...
const detail = latestProgressDetail(next);
setProgressDetail(detail);Also applies to: 142-142 🧰 Tools🪛 GitHub Check: frontend[failure] 107-107: 🤖 Prompt for AI Agents |
||
|
|
||
| function inferStage( | ||
| logs: string[], | ||
| fidelityConfirmations: number[], | ||
|
|
@@ -104,6 +139,7 @@ | |
| const [minAmount, setMinAmount] = useState<string | null>(null); | ||
| const [copied, setCopied] = useState(false); | ||
| const [errorMsg, setErrorMsg] = useState<string | null>(null); | ||
| const [progressDetail, setProgressDetail] = useState<string | null>(null); | ||
|
|
||
| const logsEndRef = useRef<HTMLDivElement>(null); | ||
| const pollRef = useRef<ReturnType<typeof setInterval> | null>(null); | ||
|
|
@@ -320,7 +356,8 @@ | |
| </div> | ||
| ), | ||
| 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 @@ | |
| </div> | ||
| ), | ||
| 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 @@ | |
| </div> | ||
| ), | ||
| 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 @@ | |
| d="M4 12a8 8 0 018-8v8z" | ||
| /> | ||
| </svg> | ||
| Watching for incoming funds — this page will update | ||
| automatically | ||
| {progressDetail ?? | ||
| "Watching for incoming funds — this page will update automatically"} | ||
| </div> | ||
| </div> | ||
| )} | ||
|
|
@@ -659,7 +700,7 @@ | |
| d="M4 12a8 8 0 018-8v8z" | ||
| /> | ||
| </svg> | ||
| Waiting for block confirmation… | ||
| {progressDetail ?? "Waiting for block confirmation…"} | ||
| </div> | ||
| </div> | ||
| )} | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.