Skip to content

fix(ui): real server-side pagination for transactions (rant 2026-08-24T10:51:57) - #135

Merged
argszero merged 1 commit into
mainfrom
fix/tx-server-pagination
Aug 24, 2026
Merged

argszero merged 1 commit into
mainfrom
fix/tx-server-pagination

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

Fix the fake-pagination bug on the transactions page (host rant 2026-08-24T10:51:57): the frontend hardcoded page=1&page_size=100 and the pager used the loaded data.length (=100) as the total, so with 7320 real records in 24h the page showed "共 100 条" and only the first 100 were ever reachable.

Changes (all frontend):

  • loadTransactions() now sends the actual txTable.page / txTable.pageSize to /api/transactions?page=N&page_size=M and records loadedPage / loadedPageSize after each fetch
  • renderTransactions() refetches the requested page when local page/pageSize differs from the loaded one — page buttons, column sort, column filters and the page-size selector all flow through this guard
  • buildDataTable() gains an optional serverPaging: { total }: the pager shows the backend real total (COUNT(*) from wallet.rs) and computes page count from it; rows are the already-paginated page (no local slice). Other tables (api-keys, members, etc.) are unaffected — they don't pass serverPaging
  • Compact pager with ellipsis window (1 … p-1 p p+1 … N) when pages > 9 (7320/10 → 732 pages; rendering 732 buttons was unusable)
  • After paging to a page > 1, the list scrolls into view

Preserved (per rant requirements):

  • Summary bar and trend chart stay full-scale (backend SQL aggregation — unchanged)
  • CSV export keeps exporting the current filtered visible rows
  • Type tab / time-range filters are kept (they are already sent as query params; each tab/range change resets to page 1)

Related Issue

Host rant 2026-08-24T10:51:57.822556+08:00 (transactions page fake pagination → server-side paging). No GitHub issue.

Tests

  • cargo test all pass (146/146)
  • cargo fmt --check pass
  • cargo clippy --all-targets -- -D warnings pass
  • node --check on modified JS

Checklist

  • Branch naming follows convention (fix/tx-server-pagination)
  • Conventional Commits
  • Single responsibility (one bug), minimal change

…4T10:51:57)

Fake pagination bug: loadTransactions hardcoded page=1&page_size=100 and the
pager used the loaded data.length (=100) as the total, so with 7320 real
records only the first 100 were ever visible.

- loadTransactions now sends txTable.page / txTable.pageSize and records
  loadedPage/loadedPageSize after each fetch
- renderTransactions refetches the requested page when local page/pageSize
  differs from the loaded one (page buttons, sort, filters, page-size all
  flow through this guard)
- buildDataTable gains optional serverPaging.total: pager shows the backend
  total and page count from it; rows are the already-paginated page (no
  local slice); other tables unaffected
- compact pager with ellipsis window (1 … p-1 p p+1 … N) when pages > 9
- scroll list into view after paging to a page > 1
- cache-bust 20260824-3
@argszero
argszero merged commit 052b60c into main Aug 24, 2026
1 check passed
@argszero
argszero deleted the fix/tx-server-pagination branch August 24, 2026 03:12
argszero added a commit that referenced this pull request Aug 25, 2026
…5T10:33:26) (#146)

Column filters (type/user/model/key/pts/status) on the transactions page
previously only filtered the currently loaded page locally, making results
untrustworthy across thousands of rows (regression of PR #135 pagination).

- backend: TxColFilters (model/user_name/key_name LIKE, status exact,
  pts_min/pts_max range) flattened into TxQuery/TxTrendQuery; tx_where()
  applies them joined to users/api_keys/keys; summary/total/trend all use
  the same filtered SQL; type whitelist now also accepts withdraw (the UI
  column filter offers it)
- frontend: loadTransactions() sends filter params on list + trend
  requests; txFilterSig() guard reloads when filters change; renderTxSummary
  always uses the now filter-aware backend summary
- test: transactions_column_filters (LIKE/exact/range/combined/trend)
- cache-bust 20260824-14

Co-authored-by: argszero <argszero@argszerodeMac-mini.local>
argszero added a commit that referenced this pull request Sep 22, 2026
)

The transactions table's column headers carry a sort arrow, and that arrow
is a claim about the WHOLE dataset: the pager below it counts the backend's
`total` (30 rows, "1 2 3 / 3 · 30 rows") and the summary card above it is a
backend aggregate. The ordering, however, only ever applied to the page in
hand.

`#135` turned the list into a server-paged table (`serverPaging` = backend
total, one page of rows per request, `LIMIT/OFFSET` over
`ORDER BY t.id DESC`), while sorting stayed where it was born in the static
prototype (`#7`, `d70e032`): client-side, over whatever rows the caller
passed in --

    if (state.sort.length) { data = data.slice().sort(...) }
    const pageRows = serverPaging ? data : data.slice(...)

a page. So clicking "Points" ▲ reordered the ten rows on screen and painted
▲, while the row that actually holds the minimum sat on page 2/3 and was
never fetched; the request never changed. The column FILTERS travelled the
other half of that road long ago (`#232` `e7bfc6f` / C2114: "local filter of
the current page" -> "backend full-dataset filter"); the sort half never did.

Fix: one claim, three carriers, and a server that renders `ORDER BY` from a
whitelist.

  * `ui/js/app.js` — `txSortParams()` projects the single sort state
    (`txTable.sort`) into the LIST request only (the trend endpoint buckets
    by time; row order means nothing to it). `txQuerySig()` now covers the
    sort, so the reload guard refetches when a header is clicked -- without
    that line the arrow would move over a list that does not, which is worse
    than the defect being fixed. The local sort steps aside when the call
    site declares `serverSort`: `if (state.sort.length && !(serverPaging &&
    serverSort))`.
  * `src/routes/wallet.rs` — `TX_SORT_KEYS` (11 keys, exactly the sortable
    columns of `TX_COLUMNS`) + `tx_sort_expr()` + `tx_order_by()`. The user
    string never reaches SQL; the `ORDER BY` fragment is rendered from the
    whitelist, always with a trailing `, t.id DESC` (a non-unique sort makes
    `LIMIT/OFFSET` page boundaries indeterminate -- one row twice, another
    never), and an unknown key / mismatched key-direction count / bad
    direction is a 400. Validation runs before the DB lock, as `type` does.
    Expressions match what the cell shows (`pts` via `signed_pts_expr`, the
    four token columns on their raw values, `model`/`key` byte-identical to
    the filter expressions).

Tests:

  * `state_gate::the_sort_indicator_and_the_order_by_share_one_source` —
    four rules, each with its own mutant leg, plus a
    `the_r164_rules_have_teeth` self-proof over self-contained mini sources
    and a `the_r164_roster_is_real` positive control. Roster == whitelist ==
    match arms, `q.sort`/`q.dir` bound in exactly one place, the guard reads
    the WHOLE condition (a `[^)]*` reader cannot see `!(a && b)`) and a
    control rule keeps the filter half of the same road honest.
  * `src/routes/wallet.rs` — three behaviour tests: the whole set is ordered
    (not the page), ties are stable across pages, and anything outside the
    whitelist is rejected.
  * jsdom probe (7 legs, both directions): on the pre-fix tree the two
    global-extremum legs and the "did the request carry sort" leg fail; on
    the landed bytes all seven pass. The two competing "fixes" (keep sorting
    the page and caption the arrow " (this page)"; stop painting the arrow)
    are rejected by the same legs.

Scope, stated honestly: the gate is lexical -- it proves the three carriers
agree and that the whitelist IS the column roster; it does not prove that
the rows on screen are globally ordered (that is the jsdom probe's job, and
there is no JS runner in `cargo test`), nor the `ORDER BY` semantics
themselves (those are the wallet behaviour tests). Recorded in
`ui/README.md`.
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