Skip to content

Runs: live row updates + < new (n) > button#2

Draft
mebassett wants to merge 1 commit intomainfrom
mebassett/live-runs-page
Draft

Runs: live row updates + < new (n) > button#2
mebassett wants to merge 1 commit intomainfrom
mebassett/live-runs-page

Conversation

@mebassett
Copy link
Copy Markdown
Owner

The Runs page now polls for status updates on the rows currently on screen, and surfaces a "< new (n) >" button when newer runs have arrived. Polls every 2s, on focus, and on visibility change. Uses straight polling via useAutoRevalidate; no SSE.

To keep visible rows stable across polls, page 1 now pins an anchor: an upper-bound run_id stored in ?anchor=… in the URL. Pagination and filters compose with the anchor naturally -- the cursor is the page boundary, the anchor is "as of when I started looking." First load resolves the anchor server-side from the newest row in the response, then the client replaces the URL to add it (no history entry, no scroll reset). Filter changes drop both anchor and cursor so a stale anchor never silently excludes matching rows.

The "< new (n) >" button counts runs newer than the anchor under the exact same filter set, capped at 99 ("99+" past the cap). Three states distinguished by newCount:

  • positive integer -> known count, badge shown.
  • 0 -> confirmed empty, button hidden.
  • null -> unknown (deep-linked URL with cursor but no anchor). Button still shown without a badge so the user has the escape hatch back to a fresh page 1.

If the user paginates forward and then clicks Previous all the way back to the most-recent page within the anchor window, the anchor resets automatically (presenter reports atFreshestEdge, the route strips cursor/direction/anchor on next render). They land on the truly newest data instead of being pinned to the original snapshot.

Architecture:

  • anchor: z.string().optional() added to RunListInputOptionsSchema and threaded through clickhouseRunsRepository.listRunIds as a run_id <= {anchor} predicate that composes with the existing cursor predicates. No change to the existing pagination semantics.
  • countRunsNewerThan on the runs repository (ClickHouse-only, same filter pipeline as the list query, LIMIT cap+1 to bound cost).
  • NextRunListPresenter resolves a default anchor from runs[0].id when none is supplied on a fresh page 1, runs the count when an anchor exists, and returns anchor, newCount, atFreshestEdge alongside the existing payload.
  • ListPagination extended with a third button that mirrors Prev/Next exactly -- LinkButton with a to= href, not a Button-with-onClick -- so middle-click, prefetching, and shortcut-key handling work identically. Shortcut: n.
  • New consts in apps/webapp/app/consts.ts: RUNS_LIST_POLL_INTERVAL_MS = 2000, NEW_RUNS_BADGE_CAP = 99.

What didn't change:

  • Loader stays non-deferred (no <Suspense> for the runs table). Initial-load UX is the Remix default: previous page during nav, browser loading on hard refresh.
  • The list query itself -- count is a separate query, run sequentially after the list. If it shows up in p95 we can Promise.all it.
  • No new endpoints; the loader returns everything in one round trip.

--- prior commit ---

Runs page now updates visible rows.

Every ~1.5s, refresh the status of the rows currently on screen on the Runs page (/orgs/:org/projects/:project/env/:env/runs) without changing which rows are shown.

1.5s is perhaps a bit aggressive.

This adds an anchor so we know the most recent run_id, which will help us implement a < new (n) > button later (we count things more recent than this).

This changes the RunListInputOptionsSchema, and
TaskRunListSearchFilters, and downstream stuff.

Closes #

✅ Checklist

  • I have followed every step in the contributing guide
  • The PR title follows the convention.
  • I ran and tested the code works

Testing

[Describe the steps you took to test this change]


Changelog

[Short description of what has changed]


Screenshots

[Screenshots]

💯

The Runs page now polls for status updates on the rows currently on
screen, and surfaces a "< new (n) >" button when newer runs have
arrived. Polls every 2s, on focus, and on visibility change. Uses
straight polling via `useAutoRevalidate`; no SSE.

To keep visible rows stable across polls, page 1 now pins an anchor:
an upper-bound `run_id` stored in `?anchor=…` in the URL. Pagination
and filters compose with the anchor naturally -- the cursor is the
page boundary, the anchor is "as of when I started looking." First
load resolves the anchor server-side from the newest row in the
response, then the client replaces the URL to add it (no history
entry, no scroll reset). Filter changes drop both anchor and cursor
so a stale anchor never silently excludes matching rows.

The "< new (n) >" button counts runs newer than the anchor under the
exact same filter set, capped at 99 ("99+" past the cap). Three
states distinguished by `newCount`:
- positive integer -> known count, badge shown.
- 0 -> confirmed empty, button hidden.
- null -> unknown (deep-linked URL with cursor but no anchor).
  Button still shown without a badge so the user has the escape hatch
  back to a fresh page 1.

If the user paginates forward and then clicks Previous all the way
back to the most-recent page within the anchor window, the anchor
resets automatically (presenter reports `atFreshestEdge`, the route
strips cursor/direction/anchor on next render). They land on the
truly newest data instead of being pinned to the original snapshot.

Architecture:
- `anchor: z.string().optional()` added to `RunListInputOptionsSchema`
  and threaded through `clickhouseRunsRepository.listRunIds` as a
  `run_id <= {anchor}` predicate that composes with the existing
  cursor predicates. No change to the existing pagination semantics.
- `countRunsNewerThan` on the runs repository (ClickHouse-only, same
  filter pipeline as the list query, LIMIT cap+1 to bound cost).
- `NextRunListPresenter` resolves a default anchor from `runs[0].id`
  when none is supplied on a fresh page 1, runs the count when an
  anchor exists, and returns `anchor`, `newCount`, `atFreshestEdge`
  alongside the existing payload.
- `ListPagination` extended with a third button that mirrors
  Prev/Next exactly -- LinkButton with a `to=` href, not a
  Button-with-onClick -- so middle-click, prefetching, and
  shortcut-key handling work identically. Shortcut: `n`.
- New consts in `apps/webapp/app/consts.ts`:
  `RUNS_LIST_POLL_INTERVAL_MS = 2000`, `NEW_RUNS_BADGE_CAP = 99`.

What didn't change:
- Loader stays non-deferred (no `<Suspense>` for the runs table).
  Initial-load UX is the Remix default: previous page during nav,
  browser loading on hard refresh.
- The list query itself -- count is a separate query, run
  sequentially after the list. If it shows up in p95 we can
  Promise.all it.
- No new endpoints; the loader returns everything in one round trip.

--- prior commit ---

Runs page now updates visible rows.

Every ~1.5s, refresh the status of the rows currently on screen on the Runs
page (`/orgs/:org/projects/:project/env/:env/runs`) without changing which
rows are shown.

1.5s is perhaps a bit aggressive.

This adds an anchor so we know the most recent run_id, which will help
us implement a < new (n) > button later (we count things more recent
than this).

This changes the RunListInputOptionsSchema, and
TaskRunListSearchFilters, and downstream stuff.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant