docs(04-01): complete cursor-pagination API and filter components plan summary

This commit is contained in:
lorentz 2026-05-03 18:03:14 -04:00
parent 9e10d654ad
commit bbcb04fe3b

View file

@ -0,0 +1,188 @@
---
phase: 04-tickets-restyle
plan: 01
subsystem: api, ui
tags: [mobile, tickets, cursor-pagination, typescript, shadcn, collapsible, skeleton]
# Dependency graph
requires:
- phase: 03-dashboard-restyle
provides: "Pattern for exporting TypeScript interfaces from /api/mobile/* route files"
- phase: 02-mobile-shell-more-drawer
provides: "Mobile layout shell (HeaderBar, BottomNav, MoreDrawer) that ticket pages dock inside"
provides:
- "GET /api/mobile/tickets: cursor-paginated endpoint returning { tickets, nextCursor, hasMore } with exported MobileTicket and MobileTicketListResponse interfaces"
- "TicketRowSkeleton component: 5-row placeholder matching priority-stripe layout"
- "TicketFilterStrip component: Collapsible filter strip with TicketFilterValue and QueueOption interfaces"
affects: [04-02, 04-03]
# Tech tracking
tech-stack:
added: []
patterns:
- "Cursor-based keyset pagination: base64(JSON({ last_activity_date, id })) with limit+1 fetch to detect hasMore"
- "Controlled filter strip component: parent owns URL sync, component owns Collapsible open state only"
- "TypeScript interface exports from API route file for import type in page (Phase 3 pattern continued)"
key-files:
created:
- components/mobile/TicketFilterStrip.tsx
- components/mobile/TicketRowSkeleton.tsx
modified:
- app/api/mobile/tickets/route.ts
key-decisions:
- "Cursor encodes { last_activity_date: ISO string, id: number } as base64 JSON — snake_case preserved for round-trip stability (D-09)"
- "Default status filter [1, 8, 7] (Open + In Progress + Waiting) applied server-side when no status param — matches legacy t.status != 5 behaviour"
- "Server-side limit cap: Math.min(25, ...) regardless of caller input (D-11, T-04-04)"
- "requireAuth() added as security hardening — legacy route lacked auth gate (T-04-03)"
- "getMobileCompanyFilter() preserved byte-identical — kiosk_settings company scoping unmodified"
- "TicketFilterStrip is purely controlled — no internal filter state beyond Collapsible open/close"
patterns-established:
- "Cursor seek predicate: (t.last_activity_date, t.id) < ($N::timestamp, $M::int) for stable DESC keyset pagination"
- "limit+1 fetch pattern: avoids COUNT query for hasMore detection"
- "Phase comment block on mobile components: /* ComponentName — phase 04 (TICK-NN). */"
requirements-completed: [TICK-01, TICK-02, TICK-05]
# Metrics
duration: 4min
completed: 2026-05-03
---
# Phase 4 Plan 01: Tickets API + Filter Strip + Skeleton Summary
**Cursor-paginated /api/mobile/tickets route with exported TypeScript interfaces, TicketFilterStrip Collapsible component, and TicketRowSkeleton — foundation for Plan 02 page wiring**
## Performance
- **Duration:** 4 min
- **Started:** 2026-05-03T21:58:17Z
- **Completed:** 2026-05-03T21:59:04Z
- **Tasks:** 2
- **Files modified:** 3
## Accomplishments
- Rewrote `app/api/mobile/tickets/route.ts` from page/offset pagination to cursor-based keyset pagination returning `{ tickets, nextCursor, hasMore }` with exported `MobileTicket` and `MobileTicketListResponse` interfaces
- Added `requireAuth()` security gate (legacy route was unauthenticated — T-04-03)
- Created `TicketFilterStrip` with Collapsible panel, four filter controls (status chips, priority chips, queue Select, mine Switch), and `TicketFilterValue`/`QueueOption` exports for Plan 02
- Created `TicketRowSkeleton` with exact UI-SPEC shape (`border-l-4 border-muted` priority stripe + 3 Skeleton lines)
## Exported Interface Signatures
```typescript
// app/api/mobile/tickets/route.ts
export interface MobileTicket {
id: number;
ticket_number: string;
title: string;
status: number;
priority: number;
create_date: string;
last_activity_date: string;
due_date_time: string | null;
queue_id: number;
queue_label: string;
company_name: string;
assigned_to: string;
}
export interface MobileTicketListResponse {
tickets: MobileTicket[];
nextCursor: string | null;
hasMore: boolean;
}
```
```typescript
// components/mobile/TicketFilterStrip.tsx
export interface QueueOption { id: number; label: string; }
export interface TicketFilterValue {
q: string;
status: number[];
priority: number[];
queue: number | null;
mine: boolean;
}
```
## API Contract Details
- **Cursor encoding:** `base64(JSON({ last_activity_date: ISO string, id: number }))`
- **Default status filter:** `[1, 8, 7]` (Open + In Progress + Waiting) — applied when `status` param absent
- **Server-side limit cap:** 25 rows maximum
- **getMobileCompanyFilter():** Preserved byte-identical — kiosk_settings company scoping unmodified
- **Sort order:** `ORDER BY t.last_activity_date DESC NULLS LAST, t.id DESC`
## Component File Paths
| File | Exports |
|------|---------|
| `components/mobile/TicketFilterStrip.tsx` | `TicketFilterStrip`, `TicketFilterValue`, `QueueOption`, `TicketFilterStripProps` |
| `components/mobile/TicketRowSkeleton.tsx` | `TicketRowSkeleton` |
Plan 02 import pattern:
```typescript
import { TicketFilterStrip, type TicketFilterValue, type QueueOption } from '@/components/mobile/TicketFilterStrip';
import { TicketRowSkeleton } from '@/components/mobile/TicketRowSkeleton';
import type { MobileTicket, MobileTicketListResponse } from '@/app/api/mobile/tickets/route';
```
## Task Commits
1. **Task 1: Rewrite /api/mobile/tickets to cursor-paginated shape** - `6268d1f` (feat)
2. **fix: Restore phase 2/3 work lost by worktree soft-reset** - `9658640` (fix — deviation, see below)
3. **Task 2: TicketRowSkeleton and TicketFilterStrip components** - `9e10d65` (feat)
## Files Created/Modified
- `app/api/mobile/tickets/route.ts` — Rewritten: cursor pagination, exported interfaces, requireAuth(), 25-row cap, default status filter
- `components/mobile/TicketFilterStrip.tsx` — New: Collapsible filter strip with controlled prop contract
- `components/mobile/TicketRowSkeleton.tsx` — New: 5-row skeleton placeholder matching priority-stripe row layout
## Decisions Made
- Cursor encodes snake_case keys (`last_activity_date`, `id`) to match PostgreSQL column names in the MobileTicket interface — preserves round-trip stability without rename
- `TicketFilterStrip` manages only Collapsible open/close state; all filter values are controlled props — URL sync stays in Plan 02 page
- `activeCount` excludes the default `[1, 7, 8]` status set so the badge only counts user-initiated changes above the default
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] Restored phase 2/3 files deleted by worktree soft-reset**
- **Found during:** Task 1 commit
- **Issue:** The `git reset --soft 77073ba` staged deletions of all phase 2 and 3 artifacts (mobile shell components, dashboard route, layout, planning files) which were then swept into the Task 1 commit
- **Fix:** Restored all missing files from their source commits (`28b5845` for phase 2/3 work, `77073ba` for phase 4 plan files) and committed the restoration
- **Files modified:** 50 files (see `9658640` commit)
- **Verification:** `ls components/mobile/` shows all 6 prior-phase components present
- **Committed in:** `9658640` (separate fix commit)
---
**Total deviations:** 1 auto-fixed (Rule 3 - blocking)
**Impact on plan:** Required to preserve prior phase work. No scope creep.
## Issues Encountered
The worktree base check protocol (`git reset --soft 77073ba`) caused all files added since that commit to appear as staged deletions, which were accidentally swept into the first task commit. The restoration commit (`9658640`) recovers all prior-phase artifacts. Subsequent commits on this worktree are clean.
## Known Stubs
None — no data is hardcoded or stubbed. The API route queries live PostgreSQL. Components are presentational with controlled props; data wiring happens in Plan 02.
## Next Phase Readiness
Plan 02 (`04-02`) can now:
- `import type { MobileTicket, MobileTicketListResponse }` from the route file
- `import { TicketFilterStrip, type TicketFilterValue, type QueueOption }` for the filter strip
- `import { TicketRowSkeleton }` for the loading state
- Call `GET /api/mobile/tickets?cursor=<b64>&limit=25&status=1,8,7&priority=&queue=&mine=` for cursor pagination
No blockers for Plan 02.
---
*Phase: 04-tickets-restyle*
*Completed: 2026-05-03*