From fa01db141121253acce4cb573d2f2c7feb997e6d Mon Sep 17 00:00:00 2001 From: yushen Date: Wed, 8 Apr 2026 13:45:47 +0800 Subject: [PATCH] fix(web,server): load all issues by paginating and returning true total count The ListIssues API returned len(resp) as "total" instead of the actual database count, and the frontend fetched at most 200 issues with no pagination. Workspaces with 200+ issues showed incomplete data in status columns (e.g. done list missing items). Backend: add CountIssues query to return the true filtered count. Frontend: auto-paginate in fetch() until all issues are loaded. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/web/features/issues/store.ts | 15 ++++++++++++--- server/internal/handler/issue.go | 13 ++++++++++++- server/pkg/db/generated/issue.sql.go | 27 +++++++++++++++++++++++++++ server/pkg/db/queries/issue.sql | 7 +++++++ 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/apps/web/features/issues/store.ts b/apps/web/features/issues/store.ts index 1e47b7d73e..f439f8bd63 100644 --- a/apps/web/features/issues/store.ts +++ b/apps/web/features/issues/store.ts @@ -30,9 +30,18 @@ export const useIssueStore = create((set, get) => ({ const isInitialLoad = get().issues.length === 0; if (isInitialLoad) set({ loading: true }); try { - const res = await api.listIssues({ limit: 200 }); - logger.info("fetched", res.issues.length, "issues"); - set({ issues: res.issues, loading: false }); + const pageSize = 200; + let allIssues: Issue[] = []; + let offset = 0; + let total = 0; + do { + const res = await api.listIssues({ limit: pageSize, offset }); + allIssues = allIssues.concat(res.issues); + total = res.total; + offset += res.issues.length; + } while (allIssues.length < total); + logger.info("fetched", allIssues.length, "issues"); + set({ issues: allIssues, loading: false }); } catch (err) { logger.error("fetch failed", err); toast.error("Failed to load issues"); diff --git a/server/internal/handler/issue.go b/server/internal/handler/issue.go index 418252e24c..c320d454c6 100644 --- a/server/internal/handler/issue.go +++ b/server/internal/handler/issue.go @@ -124,6 +124,17 @@ func (h *Handler) ListIssues(w http.ResponseWriter, r *http.Request) { return } + total, err := h.Queries.CountIssues(ctx, db.CountIssuesParams{ + WorkspaceID: parseUUID(workspaceID), + Status: statusFilter, + Priority: priorityFilter, + AssigneeID: assigneeFilter, + }) + if err != nil { + writeError(w, http.StatusInternalServerError, "failed to count issues") + return + } + prefix := h.getIssuePrefix(ctx, parseUUID(workspaceID)) resp := make([]IssueResponse, len(issues)) for i, issue := range issues { @@ -132,7 +143,7 @@ func (h *Handler) ListIssues(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]any{ "issues": resp, - "total": len(resp), + "total": total, }) } diff --git a/server/pkg/db/generated/issue.sql.go b/server/pkg/db/generated/issue.sql.go index f899eb6ebe..915d93485e 100644 --- a/server/pkg/db/generated/issue.sql.go +++ b/server/pkg/db/generated/issue.sql.go @@ -189,6 +189,33 @@ func (q *Queries) GetIssueInWorkspace(ctx context.Context, arg GetIssueInWorkspa return i, err } +const countIssues = `-- name: CountIssues :one +SELECT count(*) FROM issue +WHERE workspace_id = $1 + AND ($2::text IS NULL OR status = $2) + AND ($3::text IS NULL OR priority = $3) + AND ($4::uuid IS NULL OR assignee_id = $4) +` + +type CountIssuesParams struct { + WorkspaceID pgtype.UUID `json:"workspace_id"` + Status pgtype.Text `json:"status"` + Priority pgtype.Text `json:"priority"` + AssigneeID pgtype.UUID `json:"assignee_id"` +} + +func (q *Queries) CountIssues(ctx context.Context, arg CountIssuesParams) (int64, error) { + row := q.db.QueryRow(ctx, countIssues, + arg.WorkspaceID, + arg.Status, + arg.Priority, + arg.AssigneeID, + ) + var count int64 + err := row.Scan(&count) + return count, err +} + const listIssues = `-- name: ListIssues :many SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number FROM issue WHERE workspace_id = $1 diff --git a/server/pkg/db/queries/issue.sql b/server/pkg/db/queries/issue.sql index edc229c31c..ce89cce73f 100644 --- a/server/pkg/db/queries/issue.sql +++ b/server/pkg/db/queries/issue.sql @@ -7,6 +7,13 @@ WHERE workspace_id = $1 ORDER BY position ASC, created_at DESC LIMIT $2 OFFSET $3; +-- name: CountIssues :one +SELECT count(*) FROM issue +WHERE workspace_id = $1 + AND (sqlc.narg('status')::text IS NULL OR status = sqlc.narg('status')) + AND (sqlc.narg('priority')::text IS NULL OR priority = sqlc.narg('priority')) + AND (sqlc.narg('assignee_id')::uuid IS NULL OR assignee_id = sqlc.narg('assignee_id')); + -- name: GetIssue :one SELECT * FROM issue WHERE id = $1;