- photographer verdict/rating on photos (migration 0005), PUT endpoints
returning the fresh per-photo aggregate; FeedbackAggregate in models is
the single home of the merge policy (max rating, accept beats reject,
owner counts), used by album detail, XMP export and vote responses
- feedback rows carry share_id; per-client filter pills (incl. 'you')
scope filters/counts/overlays by id — labels are display-only
- XMP export modal with per-source checkboxes (?shares=…&own=…), share
validation, id dedup, and basename merging so RAW+JPEG pairs share one
sidecar instead of losing feedback to an unmatchable name
- grid keyboard culling: arrow cursor (clamped, outline after first use),
Space opens / closes the viewer, Enter/S toggle select, P/X/U and star
keys act on the cursor photo; keyboard votes never auto-navigate
- lightbox freezes the visible list while open, so voting a photo out of
the active filter no longer closes the viewer mid-run
- perf: memoized per-scope derivation map, content-visibility on grid
cells, lightweight /pending poll decoupled from vote patches, aggregate
recompute in one SQL statement, out-of-order response guard
- structure: AlbumPage split into components (Modal, UploadZone +
ActivityOverlay with failed-count, SharesPanel, XmpModal), shared
useEscape with typing guard, expiry year guard in endOfDayIso
- verdicts table (per link, like ratings), PUT verdict endpoint, typed Verdict enum
- share page: thumbs up/down, verdict filter with counts, view-scoped selection bar
- lightbox: per-page shortcut table (P/X/U, 1-5/0, S), ? help overlay, action
toast when a keyboard vote auto-advances
- shared useLightbox hook; single keydown subscription reading live state via ref
- album view: per-link thumbs and vote counts; share list shows accept/reject totals
- feedback queries deduped and run concurrently; verdict counts in one scan
- sha256+crc32 hashed during upload streaming; unique index per album
- duplicate content returns the existing photo (race-safe via 23505)
- client hashes locally (WebCrypto) and skips the transfer entirely for
content the album already has
- zip downloads stream S3->response directly using the stored crc32;
pre-hash photos spool once and self-heal (crc via zip, sha via reprocess)
- upload UI: overall progress bar, bytes, live speed, ETA
Rust (axum + sqlx) API and worker sharing a Postgres-backed job queue
(SKIP LOCKED, heartbeat, reaper, typed statuses), S3 storage with derived
keys and a fully private bucket, OIDC photographer login with per-request
allowlist checks, client share links with argon2 passwords and lockout,
cookie-based image authorization with sliding expiry, hand-rolled
spec-compliant streaming ZIP downloads with exact Content-Length,
React + Vite gallery frontend, single Docker image, Helm chart for
external S3 + Postgres, and Gitea CI.
Co-Authored-By: Claude <noreply@anthropic.com>