Skip to content

PortfolioPage's poll timer resets on every operator action instead of staying additive, contradicting #7082's own requirement #7230

Description

@JSONbored

Context

⚠️ Read this before starting. This is about the poll timer's schedule, not about whether a fetch
happens at all — both fetches already happen correctly today. Verify the fix with fake timers asserting
when the next automatic poll fires relative to the original schedule, per Requirements below.

#7082 ("Ledgers page and portfolio's queue-actions table never join the shared live-refresh poll
cadence," closed 2026-07-18) added polling to routes/portfolio.tsx's queue-actions items and
re-confirmed the existing requirement for its summary poll. Its own Requirements section states, verbatim:

The existing manual refresh after a successful release/requeue/pause/resume action must be preserved
(an operator's own action should still reflect immediately, not wait for the next tick) — this is
additive to the timer-based refresh, not a replacement for it.

The shipped implementation in PortfolioPage does not satisfy that: both the summary and queue-actions
polls are wired through useCallbacks that depend on refreshKey, a counter bumped after every
successful release/requeue action:

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

const loadItems = useCallback(() => loadPortfolioQueueItems(), [loadPortfolioQueueItems, refreshKey]);
const itemsResult = usePolledFetch(loadItems, pollIntervalMs);

usePolledFetch (src/lib/use-polled-fetch.ts) keys its useEffect on [loadFn, intervalMs]. Because
refreshKey changing gives loadSummary/loadItems a new function identity every time, the effect's
cleanup fires (window.clearInterval(id)), then the effect body re-runs: it calls run() immediately
and starts a brand-new window.setInterval(run, intervalMs). That is a replacement of the poll
timer — the 10-second countdown restarts from the moment of the action — not an addition to an
undisturbed periodic schedule. Concretely: if the shared cadence is every 10s and an operator releases an
item at t=23s (mid-cycle), the next scheduled poll should still land at t=30s per the "additive"
requirement; instead, because the effect restarts, it now lands at t=33s. In the pathological case of an
operator (or another automated caller acting through the same page) performing several actions within one
poll interval of each other, the periodic tick can be repeatedly deferred and never actually fire on its
own, since each action keeps re-arming a fresh intervalMs-long wait before the previous one completes.

This is a real, if usually minor, drift from what #7082 shipped as its explicit fix acceptance criterion
— the poll cadence an operator was told to expect ("every 10s, plus an immediate extra refresh on my own
action") is not what actually runs ("every 10s after my last action, whichever is later").

Requirements

  • After a successful release/requeue action bumps refreshKey, the immediate extra fetch must still
    happen (this already works and must not regress).
  • The existing periodic schedule must not be reset by that action: if the last scheduled (non-action)
    poll landed at some time T, the next scheduled poll must still land at T + pollIntervalMs, regardless
    of any action-triggered fetch that happened in between — matching Ledgers page and portfolio's queue-actions table never join the shared live-refresh poll cadence #7082's own "additive... not a
    replacement" wording.
  • This may require restructuring usePolledFetch (or PortfolioPage's use of it) so an imperative
    "refresh now" trigger is decoupled from the hook's own setInterval lifecycle — e.g. exposing a
    refresh() function from usePolledFetch itself that callers can invoke without tearing down and
    restarting the interval, rather than forcing a new interval via a changed loadFn identity. Changing
    usePolledFetch's public shape is in scope if it's the right fix; keep its existing behavior (fetch on
    mount, skip overlapping ticks, cancel on unmount) intact for every other caller (routes/index.tsx,
    routes/run-history.tsx, routes/ledgers.tsx's two polls).
  • No behavior change for routes/ledgers.tsx's LedgersPage, which does not have this bug (its
    loadLedgers/loadGovernorPauseState references are stable; the operator's own pause/resume action
    there is already reflected via a separate direct setPauseState write, not by perturbing the poll's
    loadFn — the correct "additive" pattern this issue is asking PortfolioPage to match).

Deliverables

  • PortfolioPage's summary poll no longer resets its interval schedule on a refreshKey bump
  • PortfolioPage's queue-actions poll no longer resets its interval schedule on a refreshKey bump
  • Both still perform an immediate extra fetch right after a successful action, unchanged from today
  • Regression test using fake timers: start a poll, let it run for part of an interval, trigger an
    action-driven refresh, then advance time to the original schedule's next tick and assert a poll
    fetch occurs at that original time (not pollIntervalMs after the action)

Test Coverage Requirements

apps/** is outside this repo's Codecov coverage.include scope (confirmed in PR #7082's own body), so
this is not gated by codecov/patch. The actual bar is apps/loopover-miner-ui's own Vitest suite
(npm test in that workspace) passing, plus the new regression test above actually failing against the
current refreshKey-in-useCallback-deps implementation before the fix and passing after.

Expected Outcome

PortfolioPage's summary and queue-actions polls behave exactly as #7082 specified: an operator's own
release/requeue action gets an immediate extra refresh, and the underlying DEFAULT_POLL_INTERVAL_MS
periodic schedule keeps running on its original cadence, undisturbed, exactly as it already does on every
other polled view in this app.

Links & Resources

apps/loopover-miner-ui/src/routes/portfolio.tsx (PortfolioPage), apps/loopover-miner-ui/src/lib/use-polled-fetch.ts,
apps/loopover-miner-ui/src/routes/ledgers.tsx (LedgersPage's runGovernorAction, for the working
"additive" comparison), #7082 (the issue whose own acceptance criterion this doesn't fully meet).

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.help wantedExtra attention is needed

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions