Dashboard stat cards read a field the API never returns #83
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: MEDIUM
The bug
frontend/src/pages/DashboardPage.tsx:18-25,40fires fourlistPhotos({status, limit: 1})queries and renders
meta.total— a field the backend never returns.metacontains onlynext_cursor(photos.py:148-151).All four stat cards show "—" permanently, after four wasted round trips.
Meanwhile
PhotoRepository.count_by_status(repositories/photos.py:68-74) is fully implementedwith zero call sites.
Note the
as { total?: number }cast — the type system was deliberately bypassed here. This isthe hand-written-types drift the generated-types issue addresses.
Fix
GET /api/photos/statsreturningcount_by_status. Measured at 66 ms over 50k rows via acovering scan of
ix_photo_collection_status— fine for a stats endpoint at target scale.useQueriesrather thanuseQueryinside.map()with an eslint-disable(
DashboardPage.tsx:19); it works today only becauseSTATUSESis a constant.needs_reviewphoto — the first result ofthe filtered list — rather than navigating to the filter page. It is the dashboard's only verb
and it currently under-delivers its label.
Done when
count_by_statushas a call siteReferences
frontend/src/pages/DashboardPage.tsx:18-25,40backend/app/repositories/photos.py:68-74backend/app/api/routes/photos.py:120-151Done in
88044bc.The cards read
meta.total, which the API has never returned — so they've shown a dash since the dashboard was written, and nobody could distinguish that from a collection with no photos. Rather than addingtotalto every list response (which would put aCOUNT(*)on the hot list path, and #84 is already about that query's cost), I addedGET /api/photos/stats: one grouped count replacing four list requests that were each fetching a photo row to answer a question about cardinality.PhotoRepository.count_by_statusalready existed for exactly this and had no call sites — which is how nobody had noticed its keys werestr(enum), i.e."PhotoStatus.approved", rather than values a client could look up. Fixed, and it now reports absent statuses as0rather than omitting them: a missing key renders as nothing, which is indistinguishable from a failed load, which is precisely the confusion #78 is about.Two things beyond the literal fix:
The
useQuery-inside-.mapis gone. It was a rules-of-hooks violation silenced with aneslint-disable, safe only becauseSTATUSEShappens to be a constant-length literal. Anyone making that list dynamic would have hit a genuinely baffling bug.Viewers see approved counts only, zero elsewhere. A true total would tell someone who cannot open a single unreviewed photo exactly how many there are — a small leak, but free to close and consistent with the viewer rules in
list_photos.Error state matches #78's principle: a dash means "could not find out", zero means zero, and a failure shows an explicit alert with a Retry rather than four silent dashes. 5 backend tests, 4 frontend tests.