Skip to content

feat: add cursor pagination for dag runs#1952

Merged
yottahmd merged 2 commits intomainfrom
dagrun-pgn
Apr 2, 2026
Merged

feat: add cursor pagination for dag runs#1952
yottahmd merged 2 commits intomainfrom
dagrun-pgn

Conversation

@yottahmd
Copy link
Copy Markdown
Collaborator

@yottahmd yottahmd commented Apr 2, 2026

Summary

  • add forward-only opaque cursor pagination for DAG run list APIs and generated clients
  • update the DAG Runs page to use cursor-backed infinite loading and keep batch actions scoped to loaded rows
  • drain cursor pages for dashboard and cockpit exact counts, then stabilize the exact-drain hook to avoid repeated fetch loops

Testing

  • go test ./internal/persis/filedagrun ./internal/service/frontend/api/v1 ./internal/service/coordinator ./internal/service/worker ./internal/runtime/agent ./internal/service/scheduler ./internal/cmn/telemetry ./internal/core/exec -count=1
  • cd ui && pnpm typecheck
  • cd ui && pnpm test -- --run src/features/dag-runs/hooks/tests/useBulkDAGRunSelection.test.tsx src/features/dag-runs/components/common/tests/DAGRunBatchActions.test.tsx src/features/dag-runs/components/dag-run-list/tests/DAGRunTable.test.tsx src/features/dag-runs/components/dag-run-list/tests/DAGRunGroupedView.test.tsx
  • cd ui && pnpm test -- --run src/features/cockpit/hooks/tests/useInfiniteKanban.test.tsx src/features/cockpit/components/tests/DateKanbanList.test.tsx src/features/cockpit/components/tests/DateKanbanSection.test.tsx

Summary by CodeRabbit

Release Notes

  • New Features

    • Added cursor-based pagination to DAG run listing endpoints (/dag-runs and /dag-runs/{name}) with limit and cursor query parameters for forward navigation.
    • Paginated response now includes nextCursor for fetching subsequent pages.
    • Added "Load more" functionality to the DAG runs page for efficient data browsing.
    • Improved list view to fetch complete DAG run datasets with deduplication support.
  • Bug Fixes

    • Added explicit 400 error responses for invalid or malformed pagination parameters.
  • Tests

    • Added comprehensive pagination behavior tests including cursor validation, deterministic ordering, and concurrent insertion handling.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 2, 2026

📝 Walkthrough

Walkthrough

Introduced cursor-based forward pagination for DAG-run listing endpoints. Added new DAGRunsPageResponse schema with optional nextCursor, extended query parameters with limit and cursor, updated HTTP handlers to bind and validate pagination parameters, and implemented file-backed store pagination with cursor encoding/decoding and multi-source merge via heap-based ordering. Updated frontend hooks and components to support paginated fetching and loading more results.

Changes

Cohort / File(s) Summary
API Schema & Generated Types
api/v1/api.yaml, api/v1/api.gen.go, ui/src/api/v1/schema.ts
Added OpenAPI definitions for DAGRunsPageResponse (with dagRuns and optional nextCursor), DAGRunListLimit, and DAGRunListCursor query parameters. Updated both /dag-runs and /dag-runs/{name} endpoints to accept limit and cursor query params, changed 200 responses to use DAGRunsPageResponse, and added explicit 400 responses for invalid pagination. Updated HTTP handlers to bind new query parameters and return 400 errors on binding failures.
Core Store Interface Extensions
internal/core/exec/dagrun.go
Extended DAGRunStore interface with new ListStatusesPage method for paginated reads. Added Cursor field to ListDAGRunStatusesOptions, introduced WithCursor functional option, and added DAGRunStatusPage struct containing Items and NextCursor.
File-backed Store Pagination Implementation
internal/persis/filedagrun/pagination.go, internal/persis/filedagrun/query_cursor.go
Implemented Store.ListStatusesPage with multi-source merge via min-heap, deterministic ordering by timestamp and name, and cursor-based forward-only pagination. Cursor encoding/decoding validates filter consistency, uses base64-URL encoding, and includes SHA-256 filter hash validation. Provides error type ErrInvalidQueryCursor for invalid/expired cursors.
Store Refactoring
internal/persis/filedagrun/store.go
Simplified ListStatuses to delegate to new listStatusesOrdered helper, removing explicit worker concurrency and post-aggregation sorting. Trimmed imports (sort, strings, sync, sync/atomic).
Store Test Coverage
internal/persis/filedagrun/store_test.go
Added TestListStatusesPage with subtests for deterministic ordering, duplicate detection, cursor filter validation, and stability under concurrent inserts.
HTTP Handler Updates
internal/service/frontend/api/v1/dagruns.go, internal/service/frontend/api/v1/transformer.go
Refactored list handlers to build shared dagRunListOptions via buildDAGRunListOptions, switched backing call from listDAGRuns to readDAGRunsPage, and added centralized filedagrun.ErrInvalidQueryCursor handling for 400 responses. Added toDAGRunsPageResponse transformer. Updated SSE handler to use ListStatusesPage with cursor and limit parsing.
Test Doubles — Store Mocks
internal/cmn/telemetry/collector_test.go, internal/core/exec/enqueue_retry_test.go, internal/runtime/agent/dbclient_test.go, internal/service/coordinator/handler_test.go, internal/service/scheduler/retry_scanner_test.go, internal/service/scheduler/zombie_detector_test.go, internal/service/worker/remote_handler_test.go, internal/service/frontend/api/v1/dagruns_internal_test.go
Added ListStatusesPage method implementations to test doubles (mockDAGRunStore, stubDAGRunStore, blockingDAGRunStore) to satisfy the updated DAGRunStore interface.
Frontend Pagination Hooks
ui/src/features/dag-runs/hooks/dagRunPagination.ts
Introduced new pagination module with fetchDAGRunsPage, fetchAllDAGRuns helpers, mergeUniqueDAGRuns de-duplication, and two hooks: useExactDAGRuns (full list with live fallback) and usePaginatedDAGRuns (head-page with manual "load more").
Bulk Selection & UI Components
ui/src/features/dag-runs/hooks/useBulkDAGRunSelection.ts, ui/src/features/dag-runs/components/common/DAGRunBatchActions.tsx, ui/src/features/dag-runs/components/common/__tests__/DAGRunBatchActions.test.tsx, ui/src/features/dag-runs/hooks/__tests__/useBulkDAGRunSelection.test.tsx
Renamed bulk-selection callback from selectAllMatching to selectAllLoaded and corresponding prop matchingCount to loadedCount to reflect pagination semantics. Updated test assertions and usages.
Frontend Pages & Hooks
ui/src/pages/dag-runs/index.tsx, ui/src/pages/index.tsx, ui/src/features/cockpit/hooks/useDateKanbanData.ts
Switched DAG-runs page from mutation-driven live fetching to usePaginatedDAGRuns with manual "load more" button. Updated dashboard to use useExactDAGRuns for live-aware complete list fetching. Updated date-kanban hook to use useExactDAGRuns and renamed refresh method.

Sequence Diagram

sequenceDiagram
    participant Client as Frontend Client
    participant Handler as HTTP Handler<br/>(dagruns.go)
    participant Store as File Store<br/>(pagination.go)
    participant FS as Filesystem
    participant Cursor as Cursor Helper<br/>(query_cursor.go)

    Client->>Handler: GET /dag-runs?limit=100&cursor=...
    Handler->>Handler: Bind & validate limit, cursor
    Handler->>Cursor: decodeQueryCursor(cursor, options)
    Cursor->>Cursor: Validate filter hash, version
    Cursor-->>Handler: dagRunListKey (start point)
    Handler->>Store: ListStatusesPage(ctx, options)
    Store->>FS: List day directories & indices
    FS-->>Store: Directory entries
    Store->>Store: Build iterators, merge via heap
    Store->>Store: Filter & sort by dagRunListKey
    Store->>Store: Return items + lastKey
    Store->>Cursor: encodeQueryCursor(options, lastKey)
    Cursor->>Cursor: Compute filter hash, version
    Cursor-->>Store: base64-encoded cursor
    Store-->>Handler: DAGRunStatusPage{Items, NextCursor}
    Handler->>Handler: toDAGRunsPageResponse(page)
    Handler-->>Client: 200 {dagRuns, nextCursor}
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • feat: tag search for dag-runs #1576: Modifies ListDAGRunStatusesOptions in the same core execution interface to add filtering options; these two PRs both extend the same options struct with complementary functionality (tags filtering vs. pagination cursor).
  • fix: bound live UI reads and DAG run timeouts #1827: Modifies the same DAG-run listing handlers and file-backed store read paths; changes in both PRs affect the pagination and filtering layers for DAG-run retrieval.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately captures the primary change: adding cursor-based pagination to DAG run list operations, which is the main feature across API, backend, and UI changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dagrun-pgn

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
ui/src/features/cockpit/hooks/useDateKanbanData.ts (1)

83-83: Unused useLiveConnection call appears redundant.

The useLiveConnection(isToday) call result is not used. Since useExactDAGRuns already handles live updates internally via useLiveInvalidation when liveEnabled: isLive is set, this standalone call may be unnecessary and could be removed.

🔧 Remove unused call
-  useLiveConnection(isToday);
   const { data, error, refresh } = useExactDAGRuns({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/features/cockpit/hooks/useDateKanbanData.ts` at line 83, The call to
useLiveConnection(isToday) in useDateKanbanData.ts is redundant because live
updates are already handled by useExactDAGRuns via useLiveInvalidation when
liveEnabled: isLive is passed; remove the unused useLiveConnection(isToday)
invocation (or, if it was intended to provide side-effects, move those
side-effects into the hook that already manages live updates—e.g., consolidate
into useExactDAGRuns or the useLiveInvalidation usage) so that only
useExactDAGRuns handles live invalidation and no unused hook return value
remains.
internal/persis/filedagrun/query_cursor.go (1)

82-119: Add a comment documenting why Limit and Unlimited are excluded from the filter hash.

The queryFilterHash function intentionally excludes pagination fields (Limit, Unlimited) and the cursor itself from the hash, including only semantic filter criteria. This allows cursors to remain valid when page size changes. Add a comment to the queryFilterHash function explaining this design decision to clarify it for future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/persis/filedagrun/query_cursor.go` around lines 82 - 119, Add a
brief comment at the top of the queryFilterHash function explaining that
pagination fields (e.g., Limit, Unlimited) and the cursor value are
intentionally omitted from the hash so that the hash only represents semantic
filter criteria (DAGRunID, Name, ExactName, From/To, Statuses, Tags,
AllHistory); this keeps previously issued cursors valid when page size or
pagination mode changes. Reference the function name queryFilterHash and mention
the excluded fields (Limit, Unlimited, cursor) in the comment so future
maintainers understand the design rationale.
ui/src/features/dag-runs/hooks/dagRunPagination.ts (1)

236-248: Type signature mismatch with useLiveInvalidation.

The liveMutate function has signature () => Promise<DAGRunsPageResponse>, but useLiveInvalidation expects KeyedMutator<T> which has a different signature accepting optional data and options parameters. This works at runtime because useLiveInvalidation calls mutate() with no arguments, but it's a type safety gap.

Consider using a type assertion or updating the LiveInvalidationOptions interface to accept a simpler callback type:

♻️ Suggested type assertion
-  useLiveInvalidation({
+  useLiveInvalidation<DAGRunsPageResponse>({
     enabled: enabled && liveEnabled,
-    mutate: liveMutate,
+    mutate: liveMutate as unknown as KeyedMutator<DAGRunsPageResponse>,
     matcher: (event) =>
       event.type === 'reset' || event.type === 'dagrun.changed',
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/features/dag-runs/hooks/dagRunPagination.ts` around lines 236 - 248,
The liveMutate function currently has signature () =>
Promise<DAGRunsPageResponse> but useLiveInvalidation expects a KeyedMutator<T>;
change liveMutate to match KeyedMutator<DAGRunsPageResponse> by either (A)
updating its declaration to accept the optional (data?, opts?) parameters and
return Promise<DAGRunsPageResponse>, or (B) coerce it when passed to
useLiveInvalidation (cast liveMutate as unknown as
KeyedMutator<DAGRunsPageResponse>) so the types align; alternatively, update the
LiveInvalidationOptions/matcher type to accept a simpler callback signature—but
prefer adjusting liveMutate to include the optional parameters to preserve type
safety (reference: liveMutate, useLiveInvalidation, KeyedMutator,
LiveInvalidationOptions).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/src/features/cockpit/hooks/useDateKanbanData.ts`:
- Line 117: The isLoading flag in useDateKanbanData is currently derived from
data.length === 0 && !typedError which treats an empty result set as "loading";
update the logic to use the loading state returned by useExactDAGRuns instead
(use the hook's isLoading or similar boolean) and remove the data.length check
so that an empty data array is rendered as "no results" rather than a loading
state; locate the isLoading assignment in useDateKanbanData and replace its
expression to reference the exactDAGRunsLoading value from useExactDAGRuns while
keeping typedError handling intact.

In `@ui/src/features/dag-runs/hooks/dagRunPagination.ts`:
- Around line 1-16: This file is missing the GPL v3 license header; run the
project's license tooling (make addlicense) to insert the standard GPL v3 header
at the top of the file and commit the change so the file containing symbols like
DAGRunSummary, DAGRunsPageResponse, DAGRunListQuery and MAX_DAG_RUN_PAGE_LIMIT
gets the required header; if your repo requires a manual header, prepend the
canonical GPL v3 comment block exactly as used across other TypeScript files in
the codebase to the top of this file.
- Around line 314-349: The loadMore function can throw on network errors because
client.GET may reject; wrap the client.GET and subsequent response handling in a
try/catch/finally: in try await client.GET('/dag-runs', ...) and process
response as before; in catch capture the thrown error, convert to a readable
message and call setLoadMoreError(message); in finally ensure
setIsLoadingMore(false) always runs; keep existing logic that updates
setOlderRuns via mergeUniqueDAGRuns and setContinuationCursorOverride when
response is successful.

In `@ui/src/pages/dag-runs/index.tsx`:
- Around line 416-436: The dagRunQuery memo misses config.tzOffsetInSec in its
dependency list causing stale fromDate/toDate when timezone offset changes;
update the dependency array for the React.useMemo that defines dagRunQuery to
include config.tzOffsetInSec so that calling formatDateForApi (which reads
config.tzOffsetInSec) will trigger recomputation; locate the dagRunQuery
declaration and add config.tzOffsetInSec to the dependencies alongside
apiDagRunId, apiFromDate, apiSearchText, apiStatus, apiTags, apiToDate, and
appBarContext.selectedRemoteNode.

---

Nitpick comments:
In `@internal/persis/filedagrun/query_cursor.go`:
- Around line 82-119: Add a brief comment at the top of the queryFilterHash
function explaining that pagination fields (e.g., Limit, Unlimited) and the
cursor value are intentionally omitted from the hash so that the hash only
represents semantic filter criteria (DAGRunID, Name, ExactName, From/To,
Statuses, Tags, AllHistory); this keeps previously issued cursors valid when
page size or pagination mode changes. Reference the function name
queryFilterHash and mention the excluded fields (Limit, Unlimited, cursor) in
the comment so future maintainers understand the design rationale.

In `@ui/src/features/cockpit/hooks/useDateKanbanData.ts`:
- Line 83: The call to useLiveConnection(isToday) in useDateKanbanData.ts is
redundant because live updates are already handled by useExactDAGRuns via
useLiveInvalidation when liveEnabled: isLive is passed; remove the unused
useLiveConnection(isToday) invocation (or, if it was intended to provide
side-effects, move those side-effects into the hook that already manages live
updates—e.g., consolidate into useExactDAGRuns or the useLiveInvalidation usage)
so that only useExactDAGRuns handles live invalidation and no unused hook return
value remains.

In `@ui/src/features/dag-runs/hooks/dagRunPagination.ts`:
- Around line 236-248: The liveMutate function currently has signature () =>
Promise<DAGRunsPageResponse> but useLiveInvalidation expects a KeyedMutator<T>;
change liveMutate to match KeyedMutator<DAGRunsPageResponse> by either (A)
updating its declaration to accept the optional (data?, opts?) parameters and
return Promise<DAGRunsPageResponse>, or (B) coerce it when passed to
useLiveInvalidation (cast liveMutate as unknown as
KeyedMutator<DAGRunsPageResponse>) so the types align; alternatively, update the
LiveInvalidationOptions/matcher type to accept a simpler callback signature—but
prefer adjusting liveMutate to include the optional parameters to preserve type
safety (reference: liveMutate, useLiveInvalidation, KeyedMutator,
LiveInvalidationOptions).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3d387320-d972-4ad8-aea2-9a9b87d93fc9

📥 Commits

Reviewing files that changed from the base of the PR and between 7f6de95 and d1a445c.

📒 Files selected for processing (26)
  • api/v1/api.gen.go
  • api/v1/api.yaml
  • internal/cmn/telemetry/collector_test.go
  • internal/core/exec/dagrun.go
  • internal/core/exec/enqueue_retry_test.go
  • internal/persis/filedagrun/pagination.go
  • internal/persis/filedagrun/query_cursor.go
  • internal/persis/filedagrun/store.go
  • internal/persis/filedagrun/store_test.go
  • internal/runtime/agent/dbclient_test.go
  • internal/service/coordinator/handler_test.go
  • internal/service/frontend/api/v1/dagruns.go
  • internal/service/frontend/api/v1/dagruns_internal_test.go
  • internal/service/frontend/api/v1/transformer.go
  • internal/service/scheduler/retry_scanner_test.go
  • internal/service/scheduler/zombie_detector_test.go
  • internal/service/worker/remote_handler_test.go
  • ui/src/api/v1/schema.ts
  • ui/src/features/cockpit/hooks/useDateKanbanData.ts
  • ui/src/features/dag-runs/components/common/DAGRunBatchActions.tsx
  • ui/src/features/dag-runs/components/common/__tests__/DAGRunBatchActions.test.tsx
  • ui/src/features/dag-runs/hooks/__tests__/useBulkDAGRunSelection.test.tsx
  • ui/src/features/dag-runs/hooks/dagRunPagination.ts
  • ui/src/features/dag-runs/hooks/useBulkDAGRunSelection.ts
  • ui/src/pages/dag-runs/index.tsx
  • ui/src/pages/index.tsx

columns,
error: typedError,
isLoading: !data && !typedError,
isLoading: data.length === 0 && !typedError,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

isLoading logic may conflate "loading" with "empty data".

The condition data.length === 0 && !typedError returns true both when data is still loading AND when the fetch completed with zero results. This could cause the UI to display a loading state even when there are genuinely no DAG runs for the selected date.

Consider using the isLoading state from useExactDAGRuns instead:

🐛 Proposed fix
-  const { data, error, refresh } = useExactDAGRuns({
+  const { data, error, isLoading, refresh } = useExactDAGRuns({
     query: {
       remoteNode,
       tags: tag,
       fromDate,
       toDate,
     },
     liveEnabled: isLive,
     fallbackIntervalMs: isToday ? 2000 : 0,
   });
   // ... later ...
   return {
     columns,
     error: typedError,
-    isLoading: data.length === 0 && !typedError,
+    isLoading,
     isEmpty,
     retry: refresh,
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
isLoading: data.length === 0 && !typedError,
const { data, error, isLoading, refresh } = useExactDAGRuns({
query: {
remoteNode,
tags: tag,
fromDate,
toDate,
},
liveEnabled: isLive,
fallbackIntervalMs: isToday ? 2000 : 0,
});
// ... other code ...
return {
columns,
error: typedError,
isLoading,
isEmpty,
retry: refresh,
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/features/cockpit/hooks/useDateKanbanData.ts` at line 117, The
isLoading flag in useDateKanbanData is currently derived from data.length === 0
&& !typedError which treats an empty result set as "loading"; update the logic
to use the loading state returned by useExactDAGRuns instead (use the hook's
isLoading or similar boolean) and remove the data.length check so that an empty
data array is rendered as "no results" rather than a loading state; locate the
isLoading assignment in useDateKanbanData and replace its expression to
reference the exactDAGRunsLoading value from useExactDAGRuns while keeping
typedError handling intact.

Comment on lines +1 to +16
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { components, paths } from '@/api/v1/schema';
import { useClient, useQuery } from '@/hooks/api';
import {
liveFallbackOptions,
useLiveConnection,
useLiveDAGRuns,
useLiveInvalidation,
} from '@/hooks/useAppLive';
import { isAbortLikeError } from '@/lib/requestTimeout';

export type DAGRunSummary = components['schemas']['DAGRunSummary'];
export type DAGRunsPageResponse = components['schemas']['DAGRunsPageResponse'];
export type DAGRunListQuery = paths['/dag-runs']['get']['parameters']['query'];

const MAX_DAG_RUN_PAGE_LIMIT = 500;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing GPL v3 license header.

This file is missing the required license header. As per coding guidelines, source files matching **/*.{go,ts,tsx,js} must apply GPL v3 license headers, managed via make addlicense.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/features/dag-runs/hooks/dagRunPagination.ts` around lines 1 - 16, This
file is missing the GPL v3 license header; run the project's license tooling
(make addlicense) to insert the standard GPL v3 header at the top of the file
and commit the change so the file containing symbols like DAGRunSummary,
DAGRunsPageResponse, DAGRunListQuery and MAX_DAG_RUN_PAGE_LIMIT gets the
required header; if your repo requires a manual header, prepend the canonical
GPL v3 comment block exactly as used across other TypeScript files in the
codebase to the top of this file.

Comment on lines +314 to +349
const loadMore = useCallback(async (): Promise<void> => {
if (isLoadingMore || !nextCursor) {
return;
}

setIsLoadingMore(true);
setLoadMoreError(null);

const response = await client.GET('/dag-runs', {
params: {
query: {
...query,
cursor: nextCursor,
},
},
});

setIsLoadingMore(false);

if (response.error) {
const message =
response.error &&
typeof response.error === 'object' &&
'message' in response.error
? String(response.error.message)
: 'Failed to load more DAG runs';
setLoadMoreError(message);
return;
}

const pageData = (response.data ?? { dagRuns: [] }) as DAGRunsPageResponse;
setOlderRuns((previous) =>
mergeUniqueDAGRuns(previous, pageData.dagRuns ?? [])
);
setContinuationCursorOverride(pageData.nextCursor ?? null);
}, [client, isLoadingMore, nextCursor, query]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add error handling for network failures in loadMore.

The client.GET call on line 322 could throw on network errors before returning a response object. The current code only handles response.error but not exceptions. This could result in uncaught promise rejections.

🛡️ Proposed fix to wrap in try/catch
   const loadMore = useCallback(async (): Promise<void> => {
     if (isLoadingMore || !nextCursor) {
       return;
     }

     setIsLoadingMore(true);
     setLoadMoreError(null);

-    const response = await client.GET('/dag-runs', {
-      params: {
-        query: {
-          ...query,
-          cursor: nextCursor,
+    try {
+      const response = await client.GET('/dag-runs', {
+        params: {
+          query: {
+            ...query,
+            cursor: nextCursor,
+          },
         },
-      },
-    });
+      });

-    setIsLoadingMore(false);
+      if (response.error) {
+        const message =
+          response.error &&
+          typeof response.error === 'object' &&
+          'message' in response.error
+            ? String(response.error.message)
+            : 'Failed to load more DAG runs';
+        setLoadMoreError(message);
+        return;
+      }

-    if (response.error) {
-      const message =
-        response.error &&
-        typeof response.error === 'object' &&
-        'message' in response.error
-          ? String(response.error.message)
-          : 'Failed to load more DAG runs';
-      setLoadMoreError(message);
-      return;
+      const pageData = (response.data ?? { dagRuns: [] }) as DAGRunsPageResponse;
+      setOlderRuns((previous) =>
+        mergeUniqueDAGRuns(previous, pageData.dagRuns ?? [])
+      );
+      setContinuationCursorOverride(pageData.nextCursor ?? null);
+    } catch (error) {
+      setLoadMoreError(
+        error instanceof Error ? error.message : 'Failed to load more DAG runs'
+      );
+    } finally {
+      setIsLoadingMore(false);
     }
-
-    const pageData = (response.data ?? { dagRuns: [] }) as DAGRunsPageResponse;
-    setOlderRuns((previous) =>
-      mergeUniqueDAGRuns(previous, pageData.dagRuns ?? [])
-    );
-    setContinuationCursorOverride(pageData.nextCursor ?? null);
   }, [client, isLoadingMore, nextCursor, query]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/features/dag-runs/hooks/dagRunPagination.ts` around lines 314 - 349,
The loadMore function can throw on network errors because client.GET may reject;
wrap the client.GET and subsequent response handling in a try/catch/finally: in
try await client.GET('/dag-runs', ...) and process response as before; in catch
capture the thrown error, convert to a readable message and call
setLoadMoreError(message); in finally ensure setIsLoadingMore(false) always
runs; keep existing logic that updates setOlderRuns via mergeUniqueDAGRuns and
setContinuationCursorOverride when response is successful.

Comment on lines +416 to 436
const dagRunQuery = React.useMemo(
() => ({
remoteNode: appBarContext.selectedRemoteNode || 'local',
name: apiSearchText || undefined,
dagRunId: apiDagRunId || undefined,
status: apiStatus !== 'all' ? parseInt(apiStatus) : undefined,
tags: apiTags.length > 0 ? apiTags.join(',') : undefined,
fromDate: formatDateForApi(apiFromDate),
toDate: formatDateForApi(apiToDate),
limit: 100,
}),
[
apiDagRunId,
apiFromDate,
apiSearchText,
apiStatus,
apiTags,
apiToDate,
appBarContext.selectedRemoteNode,
]
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the target file and the code around lines 416-436
head -450 ui/src/pages/dag-runs/index.tsx | tail -100

Repository: dagu-org/dagu

Length of output: 2649


🏁 Script executed:

# Let's also find and read the formatDateForApi function
rg -A 10 "formatDateForApi" ui/src/pages/dag-runs/index.tsx

Repository: dagu-org/dagu

Length of output: 758


🏁 Script executed:

# Check for the config import and how it's used
head -50 ui/src/pages/dag-runs/index.tsx

Repository: dagu-org/dagu

Length of output: 2148


🏁 Script executed:

# Search for formatDateForApi definition in the codebase
rg -l "export.*formatDateForApi|function formatDateForApi" ui/

Repository: dagu-org/dagu

Length of output: 39


🏁 Script executed:

# Let's see more of the formatDateForApi function and find where config comes from
rg -B 5 -A 15 "const formatDateForApi" ui/src/pages/dag-runs/index.tsx

Repository: dagu-org/dagu

Length of output: 768


🏁 Script executed:

# Search for useConfig hook usage in this file
rg "useConfig" ui/src/pages/dag-runs/index.tsx

Repository: dagu-org/dagu

Length of output: 144


🏁 Script executed:

# Check if config is reactive/updates when tzOffsetInSec changes
rg -A 5 "tzOffsetInSec" ui/src/contexts/ConfigContext.tsx

Repository: dagu-org/dagu

Length of output: 232


🏁 Script executed:

# Verify whether config object is mutable or if timezone changes trigger re-renders
rg -B 2 -A 10 "export.*useConfig|const.*useConfig" ui/src/contexts/

Repository: dagu-org/dagu

Length of output: 5433


🏁 Script executed:

# Get more context around formatDateForApi - let's see what's defined before it
rg -B 20 "const formatDateForApi = " ui/src/pages/dag-runs/index.tsx | head -50

Repository: dagu-org/dagu

Length of output: 642


🏁 Script executed:

# Check if formatDateForApi is actually memoized with useCallback
rg -B 3 "const formatDateForApi" ui/src/pages/dag-runs/index.tsx

Repository: dagu-org/dagu

Length of output: 181


🏁 Script executed:

# Let me see the complete context around formatDateForApi definition and dependencies
rg -B 25 "const formatDateForApi = \(" ui/src/pages/dag-runs/index.tsx | tail -40

Repository: dagu-org/dagu

Length of output: 874


🏁 Script executed:

# Let's check the full dagRunQuery memo definition to be absolutely certain about dependencies
rg -A 25 "const dagRunQuery = React.useMemo" ui/src/pages/dag-runs/index.tsx

Repository: dagu-org/dagu

Length of output: 739


🏁 Script executed:

# Let's verify the complete scope - is formatDateForApi memoized elsewhere?
rg -B 2 "formatDateForApi.*=" ui/src/pages/dag-runs/index.tsx | grep -A 5 "formatDateForApi"

Repository: dagu-org/dagu

Length of output: 85


🏁 Script executed:

# Check if config object changes are tracked or if it's static
rg -B 5 -A 10 "useUpdateConfig" ui/src/contexts/ConfigContext.tsx

Repository: dagu-org/dagu

Length of output: 251


🏁 Script executed:

# Double-check if config object reference itself is stable and only tzOffsetInSec changes
rg -A 30 "export const ConfigProvider" ui/src/contexts/ConfigContext.tsx

Repository: dagu-org/dagu

Length of output: 39


Add config.tzOffsetInSec to the dagRunQuery memo dependency array.

The dagRunQuery memo calls formatDateForApi() to compute fromDate and toDate, and formatDateForApi reads config.tzOffsetInSec. If the timezone offset changes while this page is open, the memoized query will not recompute, leaving date filters stale until another dependency (like apiFromDate) changes, causing the next fetch to use an incorrect time window.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/pages/dag-runs/index.tsx` around lines 416 - 436, The dagRunQuery memo
misses config.tzOffsetInSec in its dependency list causing stale fromDate/toDate
when timezone offset changes; update the dependency array for the React.useMemo
that defines dagRunQuery to include config.tzOffsetInSec so that calling
formatDateForApi (which reads config.tzOffsetInSec) will trigger recomputation;
locate the dagRunQuery declaration and add config.tzOffsetInSec to the
dependencies alongside apiDagRunId, apiFromDate, apiSearchText, apiStatus,
apiTags, apiToDate, and appBarContext.selectedRemoteNode.

@yottahmd yottahmd merged commit 0d7d3bf into main Apr 2, 2026
6 checks passed
@yottahmd yottahmd deleted the dagrun-pgn branch April 2, 2026 11:03
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 2, 2026

Codecov Report

❌ Patch coverage is 76.50794% with 74 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.63%. Comparing base (7f6de95) to head (d1a445c).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
internal/persis/filedagrun/pagination.go 76.85% 32 Missing and 24 partials ⚠️
internal/persis/filedagrun/query_cursor.go 75.75% 8 Missing and 8 partials ⚠️
internal/persis/filedagrun/store.go 50.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1952      +/-   ##
==========================================
+ Coverage   68.58%   68.63%   +0.05%     
==========================================
  Files         468      470       +2     
  Lines       60007    60249     +242     
==========================================
+ Hits        41153    41351     +198     
- Misses      14985    15001      +16     
- Partials     3869     3897      +28     
Files with missing lines Coverage Δ
internal/core/exec/dagrun.go 86.88% <100.00%> (+0.67%) ⬆️
internal/persis/filedagrun/store.go 70.58% <50.00%> (-4.56%) ⬇️
internal/persis/filedagrun/query_cursor.go 75.75% <75.75%> (ø)
internal/persis/filedagrun/pagination.go 76.85% <76.85%> (ø)

... and 17 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 7f6de95...d1a445c. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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