Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion apps/loopover-miner-ui/src/ledgers.test.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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<LedgersResult> => ({ ok: true, summary: fixtureSummary }));
render(
<LedgersPage
loadLedgers={loadLedgers}
loadGovernorPauseState={loadGovernorPauseStateDefault}
pollIntervalMs={1000}
/>,
);
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(
<LedgersPage
loadLedgers={async () => ({ 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)", () => {
Expand Down
23 changes: 22 additions & 1 deletion apps/loopover-miner-ui/src/portfolio-queue-actions.test.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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(
<PortfolioPage
loadPortfolioQueue={loadPortfolioQueue}
loadPortfolioQueueItems={loadPortfolioQueueItems}
pollIntervalMs={1000}
/>,
);
await vi.waitFor(() => expect(loadPortfolioQueueItems).toHaveBeenCalledTimes(1));
await vi.advanceTimersByTimeAsync(1000);
await vi.waitFor(() => expect(loadPortfolioQueueItems).toHaveBeenCalledTimes(2));
});
});
});

describe("fetchPortfolioQueueItems / release / requeue (#4857)", () => {
Expand Down
36 changes: 16 additions & 20 deletions apps/loopover-miner-ui/src/routes/ledgers.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -407,35 +408,30 @@ export function LedgersPage({
loadGovernorPauseState = fetchGovernorPauseState,
pauseGovernorAction = pauseGovernor,
resumeGovernorAction = resumeGovernor,
pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
}: {
loadLedgers?: () => Promise<LedgersResult>;
loadGovernorPauseState?: () => Promise<GovernorPauseStateResult>;
pauseGovernorAction?: (reason?: string) => Promise<GovernorPauseStateResult>;
resumeGovernorAction?: () => Promise<GovernorPauseStateResult>;
pollIntervalMs?: number;
}) {
const [result, setResult] = useState<LedgersResult | null>(null);
const [pauseState, setPauseState] = useState<GovernorPauseStateResult | null>(null);
const [lastPolledPauseState, setLastPolledPauseState] = useState<GovernorPauseStateResult | null>(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<GovernorPauseStateResult>) => {
setActionPending(true);
Expand Down
16 changes: 6 additions & 10 deletions apps/loopover-miner-ui/src/routes/portfolio.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -405,27 +405,23 @@ export function PortfolioPage({
}) {
const [refreshKey, setRefreshKey] = useState(0);
const [actionPending, setActionPending] = useState(false);
const [itemsResult, setItemsResult] = useState<PortfolioQueueItemsResult | null>(null);
const [actionResult, setActionResult] = useState<PortfolioQueueActionResult | null>(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<PortfolioQueueActionResult>) => {
setActionPending(true);
void action().then((next) => {
setActionResult(next);
if (next.ok) {
setRefreshKey((key) => key + 1);
refreshItems();
}
setActionPending(false);
});
Expand Down
Loading