From df74055d9004fc283f3d6746c4c1c33060546666 Mon Sep 17 00:00:00 2001 From: bitfathers94 <237535319+bitfathers94@users.noreply.github.com> Date: Sat, 18 Jul 2026 00:31:59 +0000 Subject: [PATCH] fix(miner-ui): join ledgers and portfolio queue-actions to the shared live-refresh poll The Ledgers page's ledger summary and governor pause-state, and the Portfolio page's queue-actions items table, each fetched only once on mount instead of joining the app's shared usePolledFetch cadence. Every other data view in the dashboard (Overview, Run History, Portfolio's summary card) auto-refreshes on DEFAULT_POLL_INTERVAL_MS, so these sections showed stale data until a manual page reload or the operator's own action. Route all three through usePolledFetch on the shared cadence. The governor pause-state is polled and synced into local state during render so an operator's own pause/resume still reflects immediately; the portfolio items poll runs independently of the summary poll so a slow or failed fetch in one section never blocks the other, and the post-action re-fetch is preserved via refreshKey. Closes #7082 --- apps/loopover-miner-ui/src/ledgers.test.tsx | 38 ++++++++++++++++++- .../src/portfolio-queue-actions.test.tsx | 23 ++++++++++- apps/loopover-miner-ui/src/routes/ledgers.tsx | 36 ++++++++---------- .../src/routes/portfolio.tsx | 16 +++----- 4 files changed, 81 insertions(+), 32 deletions(-) diff --git a/apps/loopover-miner-ui/src/ledgers.test.tsx b/apps/loopover-miner-ui/src/ledgers.test.tsx index 69fc9c57d..60f9c35a6 100644 --- a/apps/loopover-miner-ui/src/ledgers.test.tsx +++ b/apps/loopover-miner-ui/src/ledgers.test.tsx @@ -1,5 +1,5 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { fetchLedgers, LEDGERS_API_PATH, type LedgersResult, type LedgersSummary } from "./lib/ledgers"; import { type GovernorPauseState, type GovernorPauseStateResult } from "./lib/governor"; @@ -368,6 +368,42 @@ describe("LedgersPage (#4855)", () => { expect(screen.getByText(/No ledger activity yet/i)).toBeTruthy(); }); }); + + describe("live refresh (#7082)", () => { + afterEach(() => { + vi.useRealTimers(); + }); + + it("re-polls the ledger summary on the shared cadence, without any user action", async () => { + vi.useFakeTimers(); + const loadLedgers = vi.fn(async (): Promise => ({ ok: true, summary: fixtureSummary })); + render( + , + ); + await vi.waitFor(() => expect(loadLedgers).toHaveBeenCalledTimes(1)); + await vi.advanceTimersByTimeAsync(1000); + await vi.waitFor(() => expect(loadLedgers).toHaveBeenCalledTimes(2)); + }); + + it("re-polls the governor pause-state on the shared cadence, without any user action", async () => { + vi.useFakeTimers(); + const loadGovernorPauseState = vi.fn(loadGovernorPauseStateDefault); + render( + ({ ok: true, summary: fixtureSummary })} + loadGovernorPauseState={loadGovernorPauseState} + pollIntervalMs={1000} + />, + ); + await vi.waitFor(() => expect(loadGovernorPauseState).toHaveBeenCalledTimes(1)); + await vi.advanceTimersByTimeAsync(1000); + await vi.waitFor(() => expect(loadGovernorPauseState).toHaveBeenCalledTimes(2)); + }); + }); }); describe("fetchLedgers (#4855)", () => { diff --git a/apps/loopover-miner-ui/src/portfolio-queue-actions.test.tsx b/apps/loopover-miner-ui/src/portfolio-queue-actions.test.tsx index 1234e09bf..1d41ea5ed 100644 --- a/apps/loopover-miner-ui/src/portfolio-queue-actions.test.tsx +++ b/apps/loopover-miner-ui/src/portfolio-queue-actions.test.tsx @@ -1,5 +1,5 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { fetchPortfolioQueueItems, @@ -269,6 +269,27 @@ describe("PortfolioPage queue actions (#4857)", () => { expect(releaseItem).toHaveBeenCalledWith(inProgressItem); expect(loadPortfolioQueueItems).toHaveBeenCalledTimes(1); }); + + describe("live refresh (#7082)", () => { + afterEach(() => { + vi.useRealTimers(); + }); + + it("re-polls the queue-actions items on the shared cadence, without any user action", async () => { + vi.useFakeTimers(); + const loadPortfolioQueueItems = vi.fn(async () => ({ ok: true as const, items: [inProgressItem] })); + render( + , + ); + await vi.waitFor(() => expect(loadPortfolioQueueItems).toHaveBeenCalledTimes(1)); + await vi.advanceTimersByTimeAsync(1000); + await vi.waitFor(() => expect(loadPortfolioQueueItems).toHaveBeenCalledTimes(2)); + }); + }); }); describe("fetchPortfolioQueueItems / release / requeue (#4857)", () => { diff --git a/apps/loopover-miner-ui/src/routes/ledgers.tsx b/apps/loopover-miner-ui/src/routes/ledgers.tsx index a788d3496..4bb7ff69f 100644 --- a/apps/loopover-miner-ui/src/routes/ledgers.tsx +++ b/apps/loopover-miner-ui/src/routes/ledgers.tsx @@ -1,5 +1,5 @@ import { createFileRoute } from "@tanstack/react-router"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { Bar, BarChart, Cell, XAxis, YAxis } from "recharts"; import { Button } from "@loopover/ui-kit/components/button"; @@ -28,6 +28,7 @@ import { type LedgersSummary, } from "../lib/ledgers"; import { fetchGovernorPauseState, pauseGovernor, resumeGovernor, type GovernorPauseStateResult } from "../lib/governor"; +import { DEFAULT_POLL_INTERVAL_MS, usePolledFetch } from "../lib/use-polled-fetch"; export const Route = createFileRoute("/ledgers")({ component: LedgersPage, @@ -407,35 +408,30 @@ export function LedgersPage({ loadGovernorPauseState = fetchGovernorPauseState, pauseGovernorAction = pauseGovernor, resumeGovernorAction = resumeGovernor, + pollIntervalMs = DEFAULT_POLL_INTERVAL_MS, }: { loadLedgers?: () => Promise; loadGovernorPauseState?: () => Promise; pauseGovernorAction?: (reason?: string) => Promise; resumeGovernorAction?: () => Promise; + pollIntervalMs?: number; }) { - const [result, setResult] = useState(null); const [pauseState, setPauseState] = useState(null); + const [lastPolledPauseState, setLastPolledPauseState] = useState(null); const [actionPending, setActionPending] = useState(false); - useEffect(() => { - let cancelled = false; - void loadLedgers().then((loaded) => { - if (!cancelled) setResult(loaded); - }); - return () => { - cancelled = true; - }; - }, [loadLedgers]); + // Join the app's shared live-refresh cadence so newly-recorded claims/events appear without a manual reload, + // matching the Overview page's claims card that reads the same data source (#7082). + const result = usePolledFetch(loadLedgers, pollIntervalMs); - useEffect(() => { - let cancelled = false; - void loadGovernorPauseState().then((loaded) => { - if (!cancelled) setPauseState(loaded); - }); - return () => { - cancelled = true; - }; - }, [loadGovernorPauseState]); + // Poll the governor pause-state on the same cadence. Each fresh poll result is synced into `pauseState` during + // render, while the operator's own pause/resume action writes `pauseState` directly so it reflects immediately, + // not only on the next tick (#7082). + const polledPauseState = usePolledFetch(loadGovernorPauseState, pollIntervalMs); + if (polledPauseState !== lastPolledPauseState) { + setLastPolledPauseState(polledPauseState); + setPauseState(polledPauseState); + } const runGovernorAction = (action: () => Promise) => { setActionPending(true); diff --git a/apps/loopover-miner-ui/src/routes/portfolio.tsx b/apps/loopover-miner-ui/src/routes/portfolio.tsx index 2199c91f8..172be2406 100644 --- a/apps/loopover-miner-ui/src/routes/portfolio.tsx +++ b/apps/loopover-miner-ui/src/routes/portfolio.tsx @@ -1,5 +1,5 @@ import { createFileRoute } from "@tanstack/react-router"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useState } from "react"; import { Bar, BarChart, Cell, XAxis, YAxis } from "recharts"; import { Button } from "@loopover/ui-kit/components/button"; @@ -405,19 +405,16 @@ export function PortfolioPage({ }) { const [refreshKey, setRefreshKey] = useState(0); const [actionPending, setActionPending] = useState(false); - const [itemsResult, setItemsResult] = useState(null); const [actionResult, setActionResult] = useState(null); const loadSummary = useCallback(() => loadPortfolioQueue(), [loadPortfolioQueue, refreshKey]); const summaryResult = usePolledFetch(loadSummary, pollIntervalMs); - const refreshItems = useCallback(() => { - void loadPortfolioQueueItems().then(setItemsResult); - }, [loadPortfolioQueueItems, refreshKey]); - - useEffect(() => { - refreshItems(); - }, [refreshItems]); + // Poll the queue-actions items on the shared cadence too, independently of the summary card's own poll — a slow + // or failed fetch in one section must not block the other. Bumping refreshKey after the operator's own action + // re-fetches immediately, additive to the timer rather than replacing it (#7082). + const loadItems = useCallback(() => loadPortfolioQueueItems(), [loadPortfolioQueueItems, refreshKey]); + const itemsResult = usePolledFetch(loadItems, pollIntervalMs); const runQueueAction = (action: () => Promise) => { setActionPending(true); @@ -425,7 +422,6 @@ export function PortfolioPage({ setActionResult(next); if (next.ok) { setRefreshKey((key) => key + 1); - refreshItems(); } setActionPending(false); });