Priority
P2 — the audit can repeat rows and make older assignment history unreachable.
Problem
/admin/networking/ip-address-assignments does not have a cursor compatible with the API ordering:
- the UI requests
order=newest|oldest and sends from_id;
- the UI always derives the next cursor with
cursorFromDescendingPage, i.e. the minimum ID on the current page;
- the upstream
IpAddressAssignment::Index sorts by from_date DESC for newest or from_date ASC for oldest, then calls with_pagination;
- the HaveAPI implementation deployed on the test server aliases
with_pagination to ascending pagination and applies primary-key id > from_id.
Filtering by ID while ordering by an independent timestamp does not define a lossless keyset cursor. With a typical newest page containing IDs 100 through 51, the UI sends from_id=51; the backend can return IDs 100 through 52 again, and records at or below 51 remain unreachable. Non-monotonic IDs and from_date values can also cause missing rows in either sort direction.
There is a second, independent end-of-list defect: canNext = Boolean(nextCursor) enables Next for every nonempty page, including a final partial page.
Evidence
At main commit 23c71925c9d7ec314534201396ade99bd904c936:
src/pages/app/admin/networking/IpAssignmentsPage.tsx:54-75 selects the time order, sends fromId, derives the minimum-ID cursor, and enables Next for any nonempty result.
src/pages/app/admin/networking/IpAssignmentsPage.tsx:113 wires that cursor and state to the paginator.
src/lib/lockIndex.ts:65-75 defines the descending helper as the minimum ID.
- upstream
api/lib/vpsadmin/api/resources/ip_address_assignment.rb:110-120 orders by from_date and calls with_pagination.
- deployed HaveAPI 0.27.3
active_record.rb:44-60 implements with_pagination as id > from_id.
e2e/specs/admin/ip_address_assignment_exact_filter.spec.ts verifies only the first request and canonicalization. It neither traverses a second page nor asserts final-page Next state.
The underlying server-ordering class is already tracked in #189, but that issue currently enumerates dataset/snapshot/download endpoints and does not cover this page-specific cursor direction or false Next state.
Backend-contract blocker
There is no frontend-only cursor choice that can guarantee complete traversal while the server filters on ID and sorts on from_date. Changing only min ID to max ID can move forward, but can still skip an unseen lower-ID row; keeping min ID can repeat pages indefinitely. Fetching limit + 1 fixes only the false Next indicator, not completeness.
The upstream repository is read-only from this project, so do not paper over the contract in WebUI.
Recommended resolution
- Define a deterministic upstream cursor matching the selected order, preferably a stable composite
(from_date, id) cursor with both fields in the WHERE predicate and ORDER BY, or explicitly make ordering/direction ID-based if that is the intended product semantics.
- Deploy and document that API contract.
- Update the frontend to consume the matching cursor and request one-row lookahead so Next is enabled only when another row exists.
- Add integration coverage with more than two pages and deliberately non-monotonic IDs/timestamps for both newest and oldest.
- Add Playwright coverage that clicks Next, proves no duplicates or omissions, and verifies a final partial page disables Next.
Acceptance criteria
- Traversing all pages in newest and oldest order produces each matching assignment exactly once.
- Non-monotonic ID/timestamp fixtures do not skip or repeat rows.
- The final page has Next disabled.
- Exact
ip_addr, user, VPS, and active filters retain cursor correctness.
- Tests exercise actual second-page requests instead of only first-page URL canonicalization.
Related: #189, #200, #201.
Priority
P2 — the audit can repeat rows and make older assignment history unreachable.
Problem
/admin/networking/ip-address-assignmentsdoes not have a cursor compatible with the API ordering:order=newest|oldestand sendsfrom_id;cursorFromDescendingPage, i.e. the minimum ID on the current page;IpAddressAssignment::Indexsorts byfrom_date DESCfor newest orfrom_date ASCfor oldest, then callswith_pagination;with_paginationto ascending pagination and applies primary-keyid > from_id.Filtering by ID while ordering by an independent timestamp does not define a lossless keyset cursor. With a typical newest page containing IDs 100 through 51, the UI sends
from_id=51; the backend can return IDs 100 through 52 again, and records at or below 51 remain unreachable. Non-monotonic IDs andfrom_datevalues can also cause missing rows in either sort direction.There is a second, independent end-of-list defect:
canNext = Boolean(nextCursor)enables Next for every nonempty page, including a final partial page.Evidence
At main commit
23c71925c9d7ec314534201396ade99bd904c936:src/pages/app/admin/networking/IpAssignmentsPage.tsx:54-75selects the time order, sendsfromId, derives the minimum-ID cursor, and enables Next for any nonempty result.src/pages/app/admin/networking/IpAssignmentsPage.tsx:113wires that cursor and state to the paginator.src/lib/lockIndex.ts:65-75defines the descending helper as the minimum ID.api/lib/vpsadmin/api/resources/ip_address_assignment.rb:110-120orders byfrom_dateand callswith_pagination.active_record.rb:44-60implementswith_paginationasid > from_id.e2e/specs/admin/ip_address_assignment_exact_filter.spec.tsverifies only the first request and canonicalization. It neither traverses a second page nor asserts final-page Next state.The underlying server-ordering class is already tracked in #189, but that issue currently enumerates dataset/snapshot/download endpoints and does not cover this page-specific cursor direction or false Next state.
Backend-contract blocker
There is no frontend-only cursor choice that can guarantee complete traversal while the server filters on ID and sorts on
from_date. Changing only min ID to max ID can move forward, but can still skip an unseen lower-ID row; keeping min ID can repeat pages indefinitely. Fetchinglimit + 1fixes only the false Next indicator, not completeness.The upstream repository is read-only from this project, so do not paper over the contract in WebUI.
Recommended resolution
(from_date, id)cursor with both fields in theWHEREpredicate andORDER BY, or explicitly make ordering/direction ID-based if that is the intended product semantics.Acceptance criteria
ip_addr, user, VPS, and active filters retain cursor correctness.Related: #189, #200, #201.