Skip to content

Commit 9fb0baf

Browse files
authored
Merge pull request #7142 from bitfathers94/fix/issue-7082
fix(miner-ui): join ledgers and portfolio queue-actions to the shared live-refresh poll
2 parents af64808 + df74055 commit 9fb0baf

4 files changed

Lines changed: 81 additions & 32 deletions

File tree

apps/loopover-miner-ui/src/ledgers.test.tsx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2-
import { describe, expect, it, vi } from "vitest";
2+
import { afterEach, describe, expect, it, vi } from "vitest";
33

44
import { fetchLedgers, LEDGERS_API_PATH, type LedgersResult, type LedgersSummary } from "./lib/ledgers";
55
import { type GovernorPauseState, type GovernorPauseStateResult } from "./lib/governor";
@@ -368,6 +368,42 @@ describe("LedgersPage (#4855)", () => {
368368
expect(screen.getByText(/No ledger activity yet/i)).toBeTruthy();
369369
});
370370
});
371+
372+
describe("live refresh (#7082)", () => {
373+
afterEach(() => {
374+
vi.useRealTimers();
375+
});
376+
377+
it("re-polls the ledger summary on the shared cadence, without any user action", async () => {
378+
vi.useFakeTimers();
379+
const loadLedgers = vi.fn(async (): Promise<LedgersResult> => ({ ok: true, summary: fixtureSummary }));
380+
render(
381+
<LedgersPage
382+
loadLedgers={loadLedgers}
383+
loadGovernorPauseState={loadGovernorPauseStateDefault}
384+
pollIntervalMs={1000}
385+
/>,
386+
);
387+
await vi.waitFor(() => expect(loadLedgers).toHaveBeenCalledTimes(1));
388+
await vi.advanceTimersByTimeAsync(1000);
389+
await vi.waitFor(() => expect(loadLedgers).toHaveBeenCalledTimes(2));
390+
});
391+
392+
it("re-polls the governor pause-state on the shared cadence, without any user action", async () => {
393+
vi.useFakeTimers();
394+
const loadGovernorPauseState = vi.fn(loadGovernorPauseStateDefault);
395+
render(
396+
<LedgersPage
397+
loadLedgers={async () => ({ ok: true, summary: fixtureSummary })}
398+
loadGovernorPauseState={loadGovernorPauseState}
399+
pollIntervalMs={1000}
400+
/>,
401+
);
402+
await vi.waitFor(() => expect(loadGovernorPauseState).toHaveBeenCalledTimes(1));
403+
await vi.advanceTimersByTimeAsync(1000);
404+
await vi.waitFor(() => expect(loadGovernorPauseState).toHaveBeenCalledTimes(2));
405+
});
406+
});
371407
});
372408

373409
describe("fetchLedgers (#4855)", () => {

apps/loopover-miner-ui/src/portfolio-queue-actions.test.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2-
import { describe, expect, it, vi } from "vitest";
2+
import { afterEach, describe, expect, it, vi } from "vitest";
33

44
import {
55
fetchPortfolioQueueItems,
@@ -269,6 +269,27 @@ describe("PortfolioPage queue actions (#4857)", () => {
269269
expect(releaseItem).toHaveBeenCalledWith(inProgressItem);
270270
expect(loadPortfolioQueueItems).toHaveBeenCalledTimes(1);
271271
});
272+
273+
describe("live refresh (#7082)", () => {
274+
afterEach(() => {
275+
vi.useRealTimers();
276+
});
277+
278+
it("re-polls the queue-actions items on the shared cadence, without any user action", async () => {
279+
vi.useFakeTimers();
280+
const loadPortfolioQueueItems = vi.fn(async () => ({ ok: true as const, items: [inProgressItem] }));
281+
render(
282+
<PortfolioPage
283+
loadPortfolioQueue={loadPortfolioQueue}
284+
loadPortfolioQueueItems={loadPortfolioQueueItems}
285+
pollIntervalMs={1000}
286+
/>,
287+
);
288+
await vi.waitFor(() => expect(loadPortfolioQueueItems).toHaveBeenCalledTimes(1));
289+
await vi.advanceTimersByTimeAsync(1000);
290+
await vi.waitFor(() => expect(loadPortfolioQueueItems).toHaveBeenCalledTimes(2));
291+
});
292+
});
272293
});
273294

274295
describe("fetchPortfolioQueueItems / release / requeue (#4857)", () => {

apps/loopover-miner-ui/src/routes/ledgers.tsx

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createFileRoute } from "@tanstack/react-router";
2-
import { useEffect, useState } from "react";
2+
import { useState } from "react";
33
import { Bar, BarChart, Cell, XAxis, YAxis } from "recharts";
44

55
import { Button } from "@loopover/ui-kit/components/button";
@@ -28,6 +28,7 @@ import {
2828
type LedgersSummary,
2929
} from "../lib/ledgers";
3030
import { fetchGovernorPauseState, pauseGovernor, resumeGovernor, type GovernorPauseStateResult } from "../lib/governor";
31+
import { DEFAULT_POLL_INTERVAL_MS, usePolledFetch } from "../lib/use-polled-fetch";
3132

3233
export const Route = createFileRoute("/ledgers")({
3334
component: LedgersPage,
@@ -407,35 +408,30 @@ export function LedgersPage({
407408
loadGovernorPauseState = fetchGovernorPauseState,
408409
pauseGovernorAction = pauseGovernor,
409410
resumeGovernorAction = resumeGovernor,
411+
pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
410412
}: {
411413
loadLedgers?: () => Promise<LedgersResult>;
412414
loadGovernorPauseState?: () => Promise<GovernorPauseStateResult>;
413415
pauseGovernorAction?: (reason?: string) => Promise<GovernorPauseStateResult>;
414416
resumeGovernorAction?: () => Promise<GovernorPauseStateResult>;
417+
pollIntervalMs?: number;
415418
}) {
416-
const [result, setResult] = useState<LedgersResult | null>(null);
417419
const [pauseState, setPauseState] = useState<GovernorPauseStateResult | null>(null);
420+
const [lastPolledPauseState, setLastPolledPauseState] = useState<GovernorPauseStateResult | null>(null);
418421
const [actionPending, setActionPending] = useState(false);
419422

420-
useEffect(() => {
421-
let cancelled = false;
422-
void loadLedgers().then((loaded) => {
423-
if (!cancelled) setResult(loaded);
424-
});
425-
return () => {
426-
cancelled = true;
427-
};
428-
}, [loadLedgers]);
423+
// Join the app's shared live-refresh cadence so newly-recorded claims/events appear without a manual reload,
424+
// matching the Overview page's claims card that reads the same data source (#7082).
425+
const result = usePolledFetch(loadLedgers, pollIntervalMs);
429426

430-
useEffect(() => {
431-
let cancelled = false;
432-
void loadGovernorPauseState().then((loaded) => {
433-
if (!cancelled) setPauseState(loaded);
434-
});
435-
return () => {
436-
cancelled = true;
437-
};
438-
}, [loadGovernorPauseState]);
427+
// Poll the governor pause-state on the same cadence. Each fresh poll result is synced into `pauseState` during
428+
// render, while the operator's own pause/resume action writes `pauseState` directly so it reflects immediately,
429+
// not only on the next tick (#7082).
430+
const polledPauseState = usePolledFetch(loadGovernorPauseState, pollIntervalMs);
431+
if (polledPauseState !== lastPolledPauseState) {
432+
setLastPolledPauseState(polledPauseState);
433+
setPauseState(polledPauseState);
434+
}
439435

440436
const runGovernorAction = (action: () => Promise<GovernorPauseStateResult>) => {
441437
setActionPending(true);

apps/loopover-miner-ui/src/routes/portfolio.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createFileRoute } from "@tanstack/react-router";
2-
import { useCallback, useEffect, useState } from "react";
2+
import { useCallback, useState } from "react";
33
import { Bar, BarChart, Cell, XAxis, YAxis } from "recharts";
44

55
import { Button } from "@loopover/ui-kit/components/button";
@@ -405,27 +405,23 @@ export function PortfolioPage({
405405
}) {
406406
const [refreshKey, setRefreshKey] = useState(0);
407407
const [actionPending, setActionPending] = useState(false);
408-
const [itemsResult, setItemsResult] = useState<PortfolioQueueItemsResult | null>(null);
409408
const [actionResult, setActionResult] = useState<PortfolioQueueActionResult | null>(null);
410409

411410
const loadSummary = useCallback(() => loadPortfolioQueue(), [loadPortfolioQueue, refreshKey]);
412411
const summaryResult = usePolledFetch(loadSummary, pollIntervalMs);
413412

414-
const refreshItems = useCallback(() => {
415-
void loadPortfolioQueueItems().then(setItemsResult);
416-
}, [loadPortfolioQueueItems, refreshKey]);
417-
418-
useEffect(() => {
419-
refreshItems();
420-
}, [refreshItems]);
413+
// Poll the queue-actions items on the shared cadence too, independently of the summary card's own poll — a slow
414+
// or failed fetch in one section must not block the other. Bumping refreshKey after the operator's own action
415+
// re-fetches immediately, additive to the timer rather than replacing it (#7082).
416+
const loadItems = useCallback(() => loadPortfolioQueueItems(), [loadPortfolioQueueItems, refreshKey]);
417+
const itemsResult = usePolledFetch(loadItems, pollIntervalMs);
421418

422419
const runQueueAction = (action: () => Promise<PortfolioQueueActionResult>) => {
423420
setActionPending(true);
424421
void action().then((next) => {
425422
setActionResult(next);
426423
if (next.ok) {
427424
setRefreshKey((key) => key + 1);
428-
refreshItems();
429425
}
430426
setActionPending(false);
431427
});

0 commit comments

Comments
 (0)