Skip to content

ui: refetch the sandbox, node and template lists every 10 seconds - #1263

Merged
ac-pplx merged 2 commits into
mainfrom
ac/admin-ui-refresh-cadence
Nov 2, 2026
Merged

ac-pplx merged 2 commits into
mainfrom
ac/admin-ui-refresh-cadence

Conversation

@ac-pplx

@ac-pplx ac-pplx commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The sandbox, nodes and templates pages now refetch their data every 10 seconds instead of about every 2 seconds. Each tab tracks its own freshness, so every visible tab refetches on its own 10-second window and hidden tabs skip.

Their background refresh checks every second whether the view is stale, and it treated a view as stale after one second. An open sandbox list therefore refetched the full admin list (up to 1000 projected rows, the handler's busiest read) about every two seconds per browser. The templates page re-ran its several admin reads at the same rate. With the page idle for 25 seconds, the loop counted 13 list requests.

The staleness window is now 10 seconds, kept per tab: one tab's refresh no longer makes other tabs skip theirs, which would have left them showing older rows. The check itself still runs every second, which costs nothing. The cross-tab lock stays only so two tabs do not fetch at the same instant and one tab never overlaps its own fetch; a tab that loses it retries on its next check. Pages still refresh immediately after an action an operator takes on them, and the Refresh buttons are unchanged.

A sandbox created from the list is listed only once its create operation has run. The old two-second refresh used to pick it up, so the list now also refreshes when that operation completes. With "snapshots" checked, the create handler has already waited for that operation, so the list is read once and no second wait is started.

Stack

Independent of the other open console PRs. It edits the sandbox create handler next to #1249's change and the shared refresh constant that #1244 and #1252 rely on; whichever merges later may need a small rebase.

Validation

  • npm ci, npm test (95 pass), npx tsc --noEmit and npm run build in ui/, on a head rebased onto current main. A new unit test covers two tabs sharing one localStorage.
  • A Playwright loop against a local Kind stack, on main (production build) and on this branch (the Kind space-ui image). An idle sandbox list, in one tab, made 13 list requests in 25 seconds on main, and at most 4 here; these single-tab figures come from an earlier head of this branch, and two tabs were not measured in a browser.
  • Lifecycle actions on the list and on the detail page still show the new state right after they complete. A sandbox created from the list appears without a manual refresh.
  • No new console errors, page errors or failed requests compared with main.

Severity and risk

Severity: medium. The old cadence is load on the API and Postgres that grows with every open console tab.

Risk: low. Load still grows with the number of visible tabs: each visible tab makes one list read per 10-second window, where it made one about every 2 seconds. Hidden tabs make none. A change made outside the console, for example by an API client or the reconciler, now appears up to 10 seconds later instead of 2. Roll back by reverting the commits.

AI assistance: Claude Code wrote the code and this description, and ran the validation; a maintainer reviews it.

@github-actions github-actions Bot added the triage:pending Waiting for a maintainer to triage label Oct 31, 2026
@github-actions

Copy link
Copy Markdown

Thanks for opening this pull request! A SPACE maintainer will take a look.

CONTRIBUTING.md explains how we review outside contributions.

@ac-pplx
ac-pplx marked this pull request as ready for review October 31, 2026 15:11
zibi-pplx
zibi-pplx previously approved these changes Nov 1, 2026
@zibi-pplx
zibi-pplx dismissed their stale review November 1, 2026 22:30

Withdrawing this approval for now. Any feedback above still stands.

@the-gigi-pplx

Copy link
Copy Markdown
Collaborator

Review — 2 findings

Medium

  1. ui/src/lib/view-refresh.ts:7 — with two visible tabs, a list goes 20–30 s without refreshing, not the 10 s the description gives.
    The stale stamp is shared through localStorage (space:view-refresh:<key>:last, written by markViewRefreshed). But each tab keeps its own copy of the data: there's no BroadcastChannel or storage listener in ui/src. So when one tab refreshes, the other tabs skip their refresh and keep showing what they last loaded themselves. An action refresh in one tab (refresh() calls markViewRefreshed, sandboxes/page.tsx:670) pushes the other tabs back a full window too. This mechanism already exists on main, but at a 1 s window it cost the losing tab a few seconds. At 10 s the cost grows by the same factor.
    I simulated it by importing this file's shouldRefreshView and markViewRefreshed with a fake localStorage: two visible tabs ticking every 1 s with offset phases, the browser lock modelled as "skip while the other tab is mid-fetch", and 80–480 ms fetch latency. How old each tab's own data was at each tick, over 10 simulated minutes:

    Window Tab p50 p95 max
    1 s A 1.7 s 2.9 s 2.9 s
    1 s B 1.8 s 4.6 s 8.7 s
    10 s A 10.7 s 19.9 s 20.9 s
    10 s B 10.7 s 20.6 s 31.8 s

    Two ways out, each with a cost. Per-tab stamps (module state or sessionStorage) make every visible tab refetch every 10 s, so load grows with the number of open tabs. That stays below today's rate unless many tabs are open. Or keep the shared stamp and share the fetched rows too (BroadcastChannel), so the dedupe the lock already does actually saves something. That one is more code. Failing both, the risk line should say "up to 10 s per tab, longer with several tabs open".

Low

  1. ui/src/app/dashboard/sandboxes/page.tsx:799 — with "snapshots" checked, the new wait refetches the full list a second time.
    In that case settleAcceptedCreate has already awaited waitForCreate (line 777), so the operation is terminal before line 796's refreshAndReport() runs. The new waiter's first poll then returns at once and triggers a second full /v1/sandboxes?limit=1000 read. Gating the new wait on !values.snapshots removes the duplicate.

What I checked: all three pages (sandboxes, nodes, templates) read the shared constant, and each skips the tick while document.hidden, so hidden tabs cost nothing. The new waiter polls /operations/{id} every 800 ms and gives up at 30 s (public-api.ts:16-17). Its rejection path still refreshes, so a slow create shows up no later than 30 s.

Verified against 9577d11b39769c96a47f2b9e7812fe704d571fc1. Reproduced locally: npm ci, npm test (50 pass) and npm run build in ui/ of an isolated checkout, plus the two-tab simulation above. Pull CI on this head didn't build or test ui/. I didn't run the browser flow.

@the-gigi-pplx the-gigi-pplx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. 2 non-blocking findings in the review comment above.

The background refresh of the sandbox, nodes and templates pages treated
a view as stale after one second, so an open sandbox list refetched the
full admin list (up to 1000 projected rows) roughly every two seconds,
and the templates page its several admin reads at the same rate, per
browser. The staleness window is now 10 seconds. Pages still refresh
immediately after an action an operator takes on them, and the Refresh
buttons are unchanged.

A sandbox created from the list appears once its create operation has
run, which the old two-second refresh used to pick up; the list now
refreshes again when that operation completes.
…ost-create read

The stale stamp lived in localStorage while every tab keeps its own rows, so
one tab's refresh made the others skip theirs and show older data; with two
visible tabs a list could go 20-30 seconds without refreshing at a 10 second
window. The stamp is now per tab, so every visible tab refetches on its own
window (hidden tabs still skip). The cross-tab lock stays, only to stop two
tabs fetching at the same instant and a tab overlapping its own fetch.

With snapshots checked, the create handler has already waited for the create
operation, so the extra wait that refreshes the list on completion is only
started when snapshots are not requested.
@ac-pplx
ac-pplx force-pushed the ac/admin-ui-refresh-cadence branch from 9577d11 to 80483db Compare November 2, 2026 16:53
@ac-pplx

ac-pplx commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, both fixed. 80483db keeps the freshness stamp per tab, so every visible tab refreshes on its own 10 s window (the cross-tab lock only stops two tabs fetching at the same instant), with a unit test for two tabs sharing one localStorage. With "snapshots" checked the list is read once. The risk line now says load grows with visible tabs.

@ac-pplx
ac-pplx added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit d36e309 Nov 2, 2026
23 checks passed
@ac-pplx
ac-pplx deleted the ac/admin-ui-refresh-cadence branch November 2, 2026 17:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

triage:pending Waiting for a maintainer to triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants