fix: Creators endpoint returns paginated response, review queue limit raised to 1000, added GET /review/moments/{id} endpoint

- Creators: response_model changed from list to {items, total, offset, limit} matching frontend CreatorBrowseResponse
- Review queue: limit raised from 100 to 1000
- New GET /review/moments/{moment_id} endpoint for direct moment fetch
- MomentDetail uses fetchMoment instead of fetching full queue
- Merge candidates fetch uses limit=100
This commit is contained in:
jlightner 2026-03-30 01:26:12 -05:00
parent 0b0ca598b4
commit 76138887d2
4 changed files with 44 additions and 15 deletions

View file

@ -20,14 +20,14 @@ logger = logging.getLogger("chrysopedia.creators")
router = APIRouter(prefix="/creators", tags=["creators"])
@router.get("", response_model=list[CreatorBrowseItem])
@router.get("")
async def list_creators(
sort: Annotated[str, Query()] = "random",
genre: Annotated[str | None, Query()] = None,
offset: Annotated[int, Query(ge=0)] = 0,
limit: Annotated[int, Query(ge=1, le=100)] = 50,
db: AsyncSession = Depends(get_session),
) -> list[CreatorBrowseItem]:
):
"""List creators with sort, genre filter, and technique/video counts.
- **sort**: ``random`` (default, R014 creator equity), ``alpha``, ``views``
@ -80,11 +80,17 @@ async def list_creators(
CreatorBrowseItem(**base.model_dump(), technique_count=tc, video_count=vc)
)
# Get total count (without offset/limit)
count_stmt = select(func.count()).select_from(Creator)
if genre:
count_stmt = count_stmt.where(Creator.genres.any(genre))
total = (await db.execute(count_stmt)).scalar() or 0
logger.debug(
"Listed %d creators (sort=%s, genre=%s, offset=%d, limit=%d)",
len(items), sort, genre, offset, limit,
)
return items
return {"items": items, "total": total, "offset": offset, "limit": limit}
@router.get("/{slug}", response_model=CreatorDetail)

View file

@ -57,7 +57,7 @@ def _moment_to_queue_item(
async def list_queue(
status: Annotated[str, Query()] = "pending",
offset: Annotated[int, Query(ge=0)] = 0,
limit: Annotated[int, Query(ge=1, le=100)] = 50,
limit: Annotated[int, Query(ge=1, le=1000)] = 50,
db: AsyncSession = Depends(get_session),
) -> ReviewQueueResponse:
"""List key moments in the review queue, filtered by status."""
@ -313,6 +313,27 @@ async def merge_moments(
return KeyMomentRead.model_validate(source)
@router.get("/moments/{moment_id}", response_model=ReviewQueueItem)
async def get_moment(
moment_id: uuid.UUID,
db: AsyncSession = Depends(get_session),
) -> ReviewQueueItem:
"""Get a single key moment by ID with video and creator info."""
stmt = (
select(KeyMoment, SourceVideo.file_path, Creator.name)
.join(SourceVideo, KeyMoment.source_video_id == SourceVideo.id)
.join(Creator, SourceVideo.creator_id == Creator.id)
.where(KeyMoment.id == moment_id)
)
result = await db.execute(stmt)
row = result.one_or_none()
if row is None:
raise HTTPException(status_code=404, detail=f"Moment {moment_id} not found")
moment, file_path, creator_name = row
return _moment_to_queue_item(moment, file_path or "", creator_name)
@router.get("/mode", response_model=ReviewModeResponse)
async def get_mode() -> ReviewModeResponse:
"""Get the current review mode (review vs auto)."""

View file

@ -121,6 +121,12 @@ export async function fetchQueue(
);
}
export async function fetchMoment(
momentId: string,
): Promise<ReviewQueueItem> {
return request<ReviewQueueItem>(`${BASE}/moments/${momentId}`);
}
export async function fetchStats(): Promise<ReviewStatsResponse> {
return request<ReviewStatsResponse>(`${BASE}/stats`);
}

View file

@ -11,6 +11,7 @@
import { useCallback, useEffect, useState } from "react";
import { useParams, useNavigate, Link } from "react-router-dom";
import {
fetchMoment,
fetchQueue,
approveMoment,
rejectMoment,
@ -59,16 +60,11 @@ export default function MomentDetail() {
setError(null);
try {
// Fetch all moments and find the one matching our ID
const res = await fetchQueue({ limit: 500 });
const found = res.items.find((m) => m.id === momentId);
if (!found) {
setError("Moment not found");
} else {
const found = await fetchMoment(momentId);
setMoment(found);
setEditTitle(found.title);
setEditSummary(found.summary);
setEditContentType(found.content_type);
}
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load moment");
} finally {
@ -174,7 +170,7 @@ export default function MomentDetail() {
setActionError(null);
try {
// Load moments from the same video for merge candidates
const res = await fetchQueue({ limit: 500 });
const res = await fetchQueue({ limit: 100 });
const candidates = res.items.filter(
(m) => m.source_video_id === moment.source_video_id && m.id !== moment.id
);