Skip to content

feat(requests): keyset pagination for the request list (#48) - #58

Merged
Fluory merged 3 commits into
mainfrom
claude/feat-request-list-paging-48
Sep 24, 2026
Merged

Fluory merged 3 commits into
mainfrom
claude/feat-request-list-paging-48

Conversation

@Fluory

@Fluory Fluory commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Warum

Fixes #48 · Die Anfragenliste lud bisher alle Anfragen der Firma auf einmal – mit wachsender Nutzung wird die Seite langsam. Gestapelt auf #53 (neues Design); Basis claude/feat-ui-design-52, nach dessen Merge auf main umstellen.

Arbeitsstand

  • Ziel: Liste seitenweise (50), stabil bei neuen Anfragen, Filter bleiben erhalten.
  • Nicht-Ziele: Index-Migration (s. Risiken), Seitenzahlen/Gesamtzahl pro Filter.
  • Erledigt: Keyset-Pagination (created_at desc, id desc), Cursor ?after=<uuid>, Links „Ältere Anfragen“ / „Zurück zum Anfang“, Exportdaten nur für die Seite, Startseite zählt per countRequestsByStatus in der DB, Tests, CHANGELOG, frischer Review eingearbeitet, verify grün, E2E-Smokes 2/2, CI grün.
  • Offen: Merge von feat(app): visual design from prototype "RequestFlow A" #53, dann Basis auf main umstellen; Review/Merge durch den Orchestrator.
  • Annahmen: 50 pro Seite reicht für die Sachbearbeitung.
  • Nächster kleinster Schritt: Review und Merge nach feat(app): visual design from prototype "RequestFlow A" #53.

Was ist passiert (Klartext)

Die Anfragenliste zeigt jetzt höchstens 50 Anfragen auf einmal, die neuesten zuerst. Mit „Ältere Anfragen“ blättert man weiter, mit „Zurück zum Anfang“ wieder nach vorn; gesetzte Filter (Status, mögliche Duplikate) bleiben dabei erhalten. Das Blättern merkt sich die letzte gezeigte Anfrage statt einer Seitennummer – kommen währenddessen neue Anfragen herein, rutscht nichts doppelt oder verloren durch. Die Startseite zählt die offenen Anfragen jetzt direkt in der Datenbank, statt alle zu laden.

Plan-Pflicht (SYSTEM.md §4)

  • Kein Auslöser – keine Modulgrenze, öffentliche API, Migration, Auth/Rechte, kein Zahlungs-/Daten-/Infrapfad, höchstens zwei Module, keine Architekturvarianten, umkehrbar
  • Auslöser zutreffend – Impact Manifest ausgefüllt (Plan vor Code)

Geändert

  • neu src/features/requests/cursor.ts (+ Test): Cursor lesen/prüfen – alles außer genau einer UUID → Seite 1
  • src/features/requests/repository.ts, index.ts: listRequests(tx, filter, { after }) → { rows, nextCursor, firstPage } (51 holen, 50 zeigen; Cursor-Zeile in einer Abfrage mit voller Zeitgenauigkeit gelesen, fremde/unbekannte Cursor → Seite 1), countRequestsByStatus
  • src/app/requests/page.tsx: Blätter-Links mit Filtern (rel="next"), „Zurück zum Anfang“ nur auf einer positionierten Seite, Exportdaten nur für die Seite
  • src/app/page.tsx: Zählung per DB
  • Tests: tests/integration/request-list.test.ts (6 neu), tenancy.test.ts (.rows)
  • CHANGELOG.md

Nachweis (SYSTEM.md §11)

  • verify:changed: grün – request-list 12/12, Cursor-Unit-Tests 10
  • verify: grün nach den Review-Änderungen – unit 164, integration 120 (lint, typecheck, depcruise, build, audit); CI check grün
  • verify:full / E2E-Spec: review-smoke, line-items-smoke 2/2 grün (beide über /requests)
  • Manueller Prüfnachweis: –
  • Frischer Review (P1 vor Ready-for-review; Architektur/API/DB immer): feat(requests): keyset pagination for the request list (#48) #58 (comment) – nichts blockierend

Doku-Entscheidung (genau eine)

  • Keine langlebige Doku betroffen – Begründung: –
  • Doku betroffen und im selben PR aktualisiert:
    • Produktdoku (P0: README): –
    • Technische Doku: – (api.md unberührt: die API-Route nimmt nur Uploads an)
    • Architekturkarte: –
    • ADR: –
    • CHANGELOG [Unreleased] (sichtbares Feature oder Verhalten – im selben PR, nie „später")

Entferntes oder Umbenanntes: docs/ + README gegrept, Treffer bereinigt: nichts entfernt

Dateigrößen und neue Bausteine (SYSTEM.md §7)

Dateien über 500 Zeilen im Diff (Ausnahmen: generierter Code, Lockfiles, Fixtures, Migrationen, Schemas, Ressourcen, Doku, Konfiguration):

  • keine
  • bewusst belassen – Begründung: –
  • im selben PR nach fachlicher Verantwortung geteilt
  • Folge-Issue –

Über 800 Zeilen mit neuer Fachlogik oder über 1000 Zeilen (P1/P2): nicht betroffen

Neue Shared-Komponente, Utility-Datei, Adapter oder fachlicher Service:

  • nein
  • ja – gesucht nach: cursor, pagination, after, limit in src/; gefunden: nichts Gleiches – cursor.ts bleibt im Modul requests

Subagent-Einsätze

  • Umsetzungs-Subagent (Worktree) – Umsetzung, Tests, verify, E2E.
  • Frischer Review-Subagent (read-only) – nichts blockierend; 1 should (bewusst offen, s. Risiken), 3 nits behoben, 2 begründet belassen.

Risiken / offene Punkte

🤖 Generated with Claude Code

https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1

The request list shows 50 requests per page, newest first, keyset-paged on
(created_at desc, id desc). The cursor is the id of the last row; its position
is resolved inside the tenant transaction at full timestamp precision, so a
malformed, unknown or foreign cursor falls back to the first page. Filters are
kept in the "Ältere Anfragen" / "Zurück zum Anfang" links; export records are
loaded only for the rows of the current page. The start page counts requests
per status in the database instead of loading the whole list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…ositioned page (#48 review)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1

Fluory commented Sep 24, 2026

Copy link
Copy Markdown
Owner Author

Frischer Review (read-only Subagent) – Ergebnis und Umgang

Nichts blockierend. Bestätigt: Sortierung (created_at desc, id desc), Vergleich als Row-Value in SQL (keine Millisekunden-Kürzung), alle Lesezugriffe im Tenant-Kontext (Forced RLS auch für Cursor und Zählung), fremder/unbekannter Cursor → Seite 1 ohne Informationsleck, kaputte Parameter → Seite 1 statt 500, Filter-Links per URLSearchParams, Exportdaten nur für die Seite. Die Tests schlagen bei JS-Date-Vergleich, fehlendem id-Tie-Break oder Offset-Pagination fehl.

Befund Umgang
should – kein Index (company_id, created_at desc, id desc) bleibt bewusst: Migration braucht menschliche Freigabe; im PR unter Risiken als Folgeschritt vorgeschlagen
nit – Cursor-Zeile wurde zweimal gelesen (Lücke bei zwischenzeitlichem Löschen) behoben in 07ea86c: eine Abfrage liest created_at::text mit voller Genauigkeit
nit – fremder Cursor zeigte trotzdem „Zurück zum Anfang“ behoben: firstPage aus dem Repository steuert die Links; Tests prüfen es
nit – rel="next" fehlte behoben
nit – „Erneut verarbeiten“ springt auf Seite 1 ohne Filter bleibt: vorbestehend, nicht Teil von #48
nit – gültiger Cursor ohne ältere Zeilen zeigt „keine Anfragen“ bleibt: nur per handeditierter URL erreichbar

Nach den Änderungen: verify grün (unit 164, integration 120), E2E-Smokes 2/2.


Generated by Claude Code

@Fluory
Fluory marked this pull request as ready for review September 24, 2026 11:52
@Fluory
Fluory changed the base branch from claude/feat-ui-design-52 to main September 24, 2026 13:17
@Fluory
Fluory merged commit fb5045c into main Sep 24, 2026
2 checks passed
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.

feat(requests): paginate the request list

2 participants