Files
picpeak/frontend
Paul Nothaft 38c27d097c feat(faces): show a detected face in its source photo, outlined (#1120)
Phase 2 of #1096. Stacked on the phase-1 branch — it needs the Postgres fix
there, or the face list this reads comes back empty.

A 64px avatar answers "is this a person", not "is this the same person as that
other cluster". The reporter's revised use for this is the pre-merge decision:
who they were standing next to, what the occasion was. So the face opens in
its own photo with the detected box drawn, and prev/next walks that person's
other appearances without leaving the modal.

The box is positioned in PERCENTAGES of the original frame, not measured
pixels: the container carries the photo's aspect ratio, so the same four
numbers land correctly at any rendered size, with no resize listener. Verified
against real data before writing the component — bbox [221.9, 174.9, 294.5,
405.8] on a 750x750 frame resolves to left 29.6% / top 23.3% / width 39.3% /
height 54.1% and lands squarely on the face.

Preview rendition, never thumbnail, and that is load-bearing rather than a
quality preference: thumbnail_fit is seeded to 'cover' on every install, so a
thumbnail has had its edges cut off and ratios taken against the ORIGINAL land
nowhere on it. That was #1100, and it presented as a broken detector.

Not built on AdminPhotoViewer, deliberately. It wants full AdminPhoto objects
(this endpoint returns photo_id + bbox + dimensions), it carries delete and
category actions that are wrong for "who is this?", and there is no seam to
draw the box.

Three things review caught, all real:

- The container had a height cap but no width cap, so a panorama derived its
  width from the aspect ratio and overflowed the modal sideways, taking part
  of the outlined face off-screen.
- The per-tile affordance was hover-only, so on a tablet it was permanently
  invisible and there was no way to inspect a specific tile.
- The row action opened index 0, which is the TOP-SCORING face — the same
  thing as the cover only until someone uses phase 1 to pick a different one,
  at which point the row showed one face and opened another. It now resolves
  to the cover's own index.

Round 2 found three more, all real:

- The counter called a list truncated whenever it hit 500, so a person with
  exactly 500 faces was told their complete list was capped. It now compares
  against total_face_count.
- facesLoading goes false with an empty array on a zero-face person or a failed
  request, so the panel sat on a spinner that would never resolve.
- Five 32px actions plus a 64px avatar exceed a 320px row, and the name is what
  got pushed out. flex-wrap alone did not fix it — the toolbar still claimed
  its max-content width first — so its basis is capped at small sizes and the
  buttons wrap to a second line instead.

A cover that falls outside the capped list opens the first face instead. That
case implies the list IS capped, so the truncation note already explains it —
real pagination is a bigger change and is not in this.

Verified end to end: picked the 5th of 13 faces as cover, and the row action
opened at 5 / 13 rather than 1 / 13. Frontend suite 178 passing, build clean,
no new type errors.
2026-08-21 19:36:59 +02:00
..