Review

Live freight checkpoint feed uses offset as cursor

Yard ops asked for infinite scroll on the live checkpoint feed so dispatchers can page through car arrivals without full reloads. This PR renames page/limit to cursor/limit and returns next_cursor so the React board can keep scrolling as railcars ping RFID gates.

FastAPITier 4paginationkeysetconcurrencyfastapi

Click a line to flag it, pick one or more labels, then submit. If the change looks correct, approve it.

apps/yard_api/routers/checkpoints.py+13-10
4848@router.get("/yards/{yard_id}/checkpoints")
49-async def list_checkpoints(
50- yard_id: UUID,
51- page: int = Query(1, ge=1),
52- limit: int = Query(50, ge=1, le=200),
53- db: AsyncSession = Depends(get_db),
54-) -> CheckpointPage:
49+async def list_checkpoints(
50+ yard_id: UUID,
51+ cursor: str | None = Query(None, description="Opaque pagination cursor"),
52+ limit: int = Query(50, ge=1, le=200),
53+ db: AsyncSession = Depends(get_db),
54+) -> CheckpointPage:
5555 await require_yard_access(db, yard_id)
56- offset = (page - 1) * limit
56+ # cursor is the page index clients already fetched; default to first page
57+ page = int(cursor) if cursor is not None else 1
58+ offset = (page - 1) * limit
5759 stmt = (
5860 select(CheckpointEvent)
5961 .where(CheckpointEvent.yard_id == yard_id)
6062 .order_by(CheckpointEvent.recorded_at.desc(), CheckpointEvent.id.desc())
61- .offset(offset)
62- .limit(limit)
63+ .offset(offset)
64+ .limit(limit)
6365 )
6466 rows = (await db.scalars(stmt)).all()
65- return CheckpointPage(items=rows, page=page, limit=limit)
67+ next_cursor = str(page + 1) if len(rows) == limit else None
68+ return CheckpointPage(items=rows, next_cursor=next_cursor, limit=limit)