Session: 2026-02-14 10:51 — Search Pagination Production Bug Fix
Session: 2026-02-14 10:51 — Search Pagination Production Bug Fix
Summary
Investigated and fixed a production bug where search result pagination on give.berkeley.edu was broken — clicking "NEXT PAGE" loaded data but results never rendered. Also continued caching restoration work from the prior session (CSRF token migration, ac_param removal).
Work Completed
1. Caching Restoration (continued from prior session)
- Removed
ac_paramcontext processor — dead code that triggeredVary: Cookieon every response config/settings/base.py: removed from TEMPLATES context_processorsgive/context_processors.py: removedac_paramfunction-
Committed
632f6256, pushed to main -
Migrated CSRF token from server-rendered to cookie-based — enables HTML page caching
templates/base.html: removed{{ csrf_token }}fromhx-headersgive/static/assets/js/give/obf/global.js: addedhtmx:configRequestlistener to read CSRF from cookie-
Committed
4989c865, pushed to main, cherry-picked to qa -
Fixed missing csrftoken cookie — after removing
{{ csrf_token }}, Django stopped setting the cookie give/base.py: addedget_token(request)toHtmxTemplateView.dispatch()(checkout views only, non-cacheable)-
Committed
6f816532, pushed to main, cherry-picked to qa -
Regression testing: one-time gift ✓, recurring gift (6 tests) ✓, successive gifts (one-time → recurring) ✓
2. Production Search Pagination Bug (main work this session)
Symptom: On give.berkeley.edu, searching for "fund" loads page 1 correctly (759 results). Clicking "NEXT PAGE" updates pagination controls but results show "loading..." forever.
Diagnosis process:
1. Verified API works — curl to /api/searchresults?offset=10 returns valid JSON ✓
2. Created diagnostic Playwright tests injecting instrumentation into Alpine store
3. Traced execution order: nextClicked() FINISHED before updateData() FINISHED — the await wasn't working
4. Key discovery: store.nextClicked.constructor.name === "Function" (NOT AsyncFunction)
5. store.nextClicked() returns undefined (NOT a Promise) — await was a no-op
6. Root cause confirmed: production nextClicked was a regular function, not async
Root cause: Three methods in searchResultsListener.js were not async:
- nextClicked() — set loading=true, called updateData() fire-and-forget, loading never reset
- previousClicked() — same issue
- goToPage() — same issue (called from page input @focusout/@keyup.enter)
Prior fix was incomplete: Commit abe315fa (Jan 15) added async/await but removed manual page management, relying on updateCurrentPage() which has an off-by-one bug (Math.floor(offset/limit) returns 0-indexed page numbers). This fix was never deployed to prod or QA.
Corrected fix (commit d6f4703e):
- Made all three methods async with proper await this.updateData()
- Capture targetPage BEFORE calling updateData (which clobbers this.page via updateCurrentPage)
- Set this.loading = false after data loads
- Restored this.updatePager() calls (scroll-to-top)
Deployed to all branches:
| Branch | Commit | Pipeline |
|--------|--------|----------|
| main | d6f4703e | ✓ pushed |
| qa | 11e57676 | ✓ success |
| prod | ae932263 | ✓ success |
| mcp-server | d43373db | committed (not pushed) |
Post-deploy: Cloudflare was caching old JS (cf-cache-status: HIT, max-age: 1800). User purged CF cache manually. After purge, production test passed — forward (1→2→3) and backward (3→2→1) pagination all working.
3. New Test Files Created (not committed)
tests/navigation/search-pagination.spec.ts— full forward/backward pagination testtests/checkout/successive-gifts.spec.ts— one-time gift followed by recurring gift in same session
Files Modified
| File | Change |
|---|---|
give/static/assets/js/give/obf/searchResultsListener.js |
Fixed nextClicked, previousClicked, goToPage — async/await, page tracking, loading reset |
give/context_processors.py |
Removed dead ac_param function (prior session work) |
config/settings/base.py |
Removed ac_param from context_processors (prior session work) |
templates/base.html |
Removed {{ csrf_token }} from hx-headers (prior session work) |
give/static/assets/js/give/obf/global.js |
Added CSRF-from-cookie event listener (prior session work) |
give/base.py |
Added get_token(request) to HtmxTemplateView.dispatch (prior session work) |
Key Lessons
- JS obfuscation can mask bugs — production code was obfuscated;
asynckeyword preserved for some methods but not others revealed the bug wasn't in the obfuscator but in the source updateCurrentPage()returns 0-indexed pages —Math.floor(offset/limit)for offset=10, limit=10 returns 1, not 2. Manual page tracking (this.page + 1) is necessary- Cloudflare caches static assets for 30 min — after deploying JS fixes, must purge CF cache or wait for expiry
- Alpine.js async store methods —
awaitinside a non-async function is silently ignored; the function returnsundefinedinstead of a Promise, making theawaita no-op - Diagnostic technique — injecting patched methods via
page.evaluate()lets you test fixes against production without deploying
Pending Items
- Remaining caching blockers:
Vary: Cookiefrom AuthenticationMiddleware,Set-Cookieheaders on cacheable pages - CACHING.md and CLOUDFLARE_CACHING_RESTORATION.md doc corrections
- givecore December-2025 folder reorganization (staged but not committed in ~/Gitlab/givecore/)
- Commit the two new test files (
search-pagination.spec.ts,successive-gifts.spec.ts)