Context
AuditFeed (apps/loopover-ui/src/components/site/audit-feed.tsx) fetches
/v1/app/skipped-pr-audit with only a limit query parameter (built by
buildSkippedPrAuditPath in audit-feed-model.ts, lines 39–51 — the function accepts limit,
repoFullName, reason, since; there is no offset/cursor/page parameter anywhere in this
model). loadMore (line 111–113):
const loadMore = () => {
setLimit((current) => Math.min(current + DEFAULT_LIMIT, MAX_LIMIT));
};
increases limit from 50 toward a 100-item cap and re-runs the exact same query from the start via
load() (triggered by the queryPath/load dependency chain, lines 49–94) — there is no offset, so
every "Load more" click re-requests "the N most recent skip events," not "the next N after what's
already shown." For a genuinely live feed like this (new skip decisions land continuously — the
AuditFilters UI and hasMore/"More available" pill both imply an ongoing stream, not a static
export), if more than DEFAULT_LIMIT (50) new events land between the initial load and a "Load more"
click, some of the rows a user is currently looking at can silently fall out of the newly-fetched
top-limit window and disappear from the table — a real, user-visible pagination inconsistency, not
a cosmetic one.
The correct pattern already exists two files over in the same directory:
dead-letter-queue-panel-model.ts models a page as { limit, offset, total } and
dead-letter-queue-panel.tsx computes hasNext/hasPrevious from offset/limit/total and
advances/retreats by adjusting offset, not by growing a single ever-larger limit. This is a clear,
existing, in-repo precedent to follow — the "how" is not an open design question.
Requirements
- The deliverable is a real offset (or cursor) parameter, not a bigger
limit. Add
offset: number to SkippedPrAuditExport's request shape (mirroring
DeadLetterQueuePage's offset/total fields) and to buildSkippedPrAuditPath's options, and
have the backend route (/v1/app/skipped-pr-audit — locate its handler under src/ and thread the
parameter through the same way the dead-letter-queue route already does for its own offset) return
enough metadata (a total count, or a hasMore computed from offset + limit < total) to know when
to stop paging.
loadMore must advance the offset by the current page's item count (or by DEFAULT_LIMIT) and
fetch only the next page, then append the new rows to the existing data.items array in
component state rather than replacing it wholesale — so rows already rendered are never re-derived
from a re-run of the base query.
- Anti-pattern, explicit: increasing the
limit query parameter and re-issuing the same
from-the-start query — the pattern this issue exists to remove — does not satisfy this issue,
even though it currently "looks like" pagination (it produces a larger result set that visually
resembles "more rows"). A PR that only changes MAX_LIMIT or the increment size without introducing
a real offset/cursor and an append-not-replace update does not resolve this issue.
- Preserve existing filter behavior:
applyFilters/resetFilters must still reset to the first page
(offset 0) exactly as they reset limit to DEFAULT_LIMIT today.
Deliverables
Test Coverage Requirements
This touches both apps/loopover-ui/** (the AuditFeed component/model — outside Codecov's
coverage.include/apps/** ignore, so no patch-coverage obligation there, but the component's own
apps/loopover-ui/src/components/site/audit-feed.tsx/audit-feed-model.ts test files must still
cover the new append-not-replace behavior and the offset-advance math with real assertions) and
the backend route under src/ (the actual API handler for /v1/app/skipped-pr-audit) — any line/branch
changed there is subject to the 99% Codecov patch gate, hard, including both sides of any new
offset-related conditional (e.g. "more pages exist" vs. "this was the last page").
Expected Outcome
Clicking "Load more" on the skipped-PR audit feed always appends genuinely new rows after the ones
already shown, and never causes a previously-visible row to shift position or disappear, regardless
of how many new skip events have landed on the server since the initial load.
Links & Resources
apps/loopover-ui/src/components/site/audit-feed.tsx:44,49-58,111-113,251-260
apps/loopover-ui/src/components/site/audit-feed-model.ts:39-51
apps/loopover-ui/src/components/site/dead-letter-queue-panel-model.ts:10-29 (the pattern to mirror)
apps/loopover-ui/src/components/site/dead-letter-queue-panel.tsx:177,221,242,368,379
Context
AuditFeed(apps/loopover-ui/src/components/site/audit-feed.tsx) fetches/v1/app/skipped-pr-auditwith only alimitquery parameter (built bybuildSkippedPrAuditPathinaudit-feed-model.ts, lines 39–51 — the function acceptslimit,repoFullName,reason,since; there is nooffset/cursor/pageparameter anywhere in thismodel).
loadMore(line 111–113):increases
limitfrom 50 toward a 100-item cap and re-runs the exact same query from the start viaload()(triggered by thequeryPath/loaddependency chain, lines 49–94) — there is no offset, soevery "Load more" click re-requests "the N most recent skip events," not "the next N after what's
already shown." For a genuinely live feed like this (new skip decisions land continuously — the
AuditFiltersUI andhasMore/"More available" pill both imply an ongoing stream, not a staticexport), if more than
DEFAULT_LIMIT(50) new events land between the initial load and a "Load more"click, some of the rows a user is currently looking at can silently fall out of the newly-fetched
top-
limitwindow and disappear from the table — a real, user-visible pagination inconsistency, nota cosmetic one.
The correct pattern already exists two files over in the same directory:
dead-letter-queue-panel-model.tsmodels a page as{ limit, offset, total }anddead-letter-queue-panel.tsxcomputeshasNext/hasPreviousfromoffset/limit/totalandadvances/retreats by adjusting
offset, not by growing a single ever-largerlimit. This is a clear,existing, in-repo precedent to follow — the "how" is not an open design question.
Requirements
limit. Addoffset: numbertoSkippedPrAuditExport's request shape (mirroringDeadLetterQueuePage'soffset/totalfields) and tobuildSkippedPrAuditPath's options, andhave the backend route (
/v1/app/skipped-pr-audit— locate its handler undersrc/and thread theparameter through the same way the dead-letter-queue route already does for its own
offset) returnenough metadata (a
totalcount, or ahasMorecomputed fromoffset + limit < total) to know whento stop paging.
loadMoremust advance the offset by the current page's item count (or byDEFAULT_LIMIT) andfetch only the next page, then append the new rows to the existing
data.itemsarray incomponent state rather than replacing it wholesale — so rows already rendered are never re-derived
from a re-run of the base query.
limitquery parameter and re-issuing the samefrom-the-start query — the pattern this issue exists to remove — does not satisfy this issue,
even though it currently "looks like" pagination (it produces a larger result set that visually
resembles "more rows"). A PR that only changes
MAX_LIMITor the increment size without introducinga real offset/cursor and an append-not-replace update does not resolve this issue.
applyFilters/resetFiltersmust still reset to the first page(offset 0) exactly as they reset
limittoDEFAULT_LIMITtoday.Deliverables
SkippedPrAuditExport(or its request/response types) carries a real offset/cursor and enoughmetadata to compute whether more pages exist.
buildSkippedPrAuditPathaccepts and encodes the offset/cursor parameter.response).
AuditFeed'sloadMoreappends the next page's rows to existing state instead of replacing thewhole list from a bigger
limit.applyFilters/resetFiltersreset paging state to the first page.Test Coverage Requirements
This touches both
apps/loopover-ui/**(theAuditFeedcomponent/model — outside Codecov'scoverage.include/apps/**ignore, so no patch-coverage obligation there, but the component's ownapps/loopover-ui/src/components/site/audit-feed.tsx/audit-feed-model.tstest files must stillcover the new append-not-replace behavior and the offset-advance math with real assertions) and
the backend route under
src/(the actual API handler for/v1/app/skipped-pr-audit) — any line/branchchanged there is subject to the 99% Codecov
patchgate, hard, including both sides of any newoffset-related conditional (e.g. "more pages exist" vs. "this was the last page").Expected Outcome
Clicking "Load more" on the skipped-PR audit feed always appends genuinely new rows after the ones
already shown, and never causes a previously-visible row to shift position or disappear, regardless
of how many new skip events have landed on the server since the initial load.
Links & Resources
apps/loopover-ui/src/components/site/audit-feed.tsx:44,49-58,111-113,251-260apps/loopover-ui/src/components/site/audit-feed-model.ts:39-51apps/loopover-ui/src/components/site/dead-letter-queue-panel-model.ts:10-29(the pattern to mirror)apps/loopover-ui/src/components/site/dead-letter-queue-panel.tsx:177,221,242,368,379