From c64a97d147ea50f89bb93b5d7b5a5699914926b1 Mon Sep 17 00:00:00 2001 From: ilia Date: Tue, 4 Aug 2026 12:20:39 -0400 Subject: [PATCH] Add person email/phone, approve bulk select, and pending-queue digest Capture optional contact fields on identify/approve, add Select All and Approve Next 10 on User Identified Faces, and email admins when the pending queue grows (ADMIN_NOTIFY_EMAIL via existing noreply SMTP). --- .gitignore | 2 + DEPLOYMENT_CHECKLIST.md | 5 + ROADMAP.md | 3 + admin-frontend/src/api/faces.ts | 2 + .../src/api/pendingIdentifications.ts | 2 + admin-frontend/src/api/people.ts | 6 + admin-frontend/src/api/videos.ts | 2 + .../src/pages/ApproveIdentified.tsx | 80 +++++++++++ admin-frontend/src/pages/Help.tsx | 9 +- admin-frontend/src/pages/Identify.tsx | 90 +++++++++++- admin-frontend/src/pages/Modify.tsx | 28 ++++ backend/api/faces.py | 4 + backend/api/pending_identifications.py | 23 ++- backend/api/people.py | 8 ++ backend/api/videos.py | 2 + backend/app.py | 76 ++++++++++ backend/db/models.py | 2 + backend/schemas/faces.py | 2 + backend/schemas/people.py | 8 ++ backend/schemas/videos.py | 2 + backend/services/video_service.py | 13 ++ backend/worker.py | 8 ++ docs/PERSON_CONTACT_AND_PENDING_DIGEST.md | 31 ++++ docs/README.md | 4 +- docs/USER_GUIDE.md | 4 +- migrations/add-people-contact-columns.sql | 11 ++ viewer-frontend/.env.example | 8 ++ viewer-frontend/.gitignore | 3 + viewer-frontend/EMAIL_VERIFICATION_SETUP.md | 12 +- .../app/api/faces/[id]/identify/route.ts | 55 +++++-- viewer-frontend/app/api/people/route.ts | 6 + viewer-frontend/app/search/page.tsx | 4 + .../components/IdentifyFaceDialog.tsx | 38 +++++ .../components/PhotoViewerClient.tsx | 2 + viewer-frontend/lib/email.ts | 134 ++++++++++++++++++ ...pending-identification-contact-columns.sql | 12 ++ viewer-frontend/prisma/schema-auth.prisma | 2 + viewer-frontend/prisma/schema.prisma | 2 + 38 files changed, 682 insertions(+), 23 deletions(-) create mode 100644 docs/PERSON_CONTACT_AND_PENDING_DIGEST.md create mode 100644 migrations/add-people-contact-columns.sql create mode 100644 viewer-frontend/migrations/add-pending-identification-contact-columns.sql diff --git a/.gitignore b/.gitignore index a608dd1..95a42d0 100644 --- a/.gitignore +++ b/.gitignore @@ -101,3 +101,5 @@ e2e/.env logs/ .vite/ e2e/env-defaults.local.json + +viewer-frontend/.data/ diff --git a/DEPLOYMENT_CHECKLIST.md b/DEPLOYMENT_CHECKLIST.md index 9244f06..f743a40 100644 --- a/DEPLOYMENT_CHECKLIST.md +++ b/DEPLOYMENT_CHECKLIST.md @@ -46,6 +46,11 @@ SMTP_PASS=your-mailbox-password SMTP_FROM_EMAIL=noreply@your-domain.com SMTP_FROM_NAME=PunimTag Viewer +# Optional: digest when pending face-IDs queue grows (same SMTP/Resend transport) +# ADMIN_NOTIFY_EMAIL=you@example.com +# PENDING_QUEUE_DIGEST_THRESHOLD=1 +# PENDING_QUEUE_DIGEST_COOLDOWN_MINUTES=60 + # Option B: Resend only (set EMAIL_PROVIDER=resend, or leave SMTP_* unset above) RESEND_API_KEY=re_xxx RESEND_FROM_EMAIL=onboarding@resend.dev diff --git a/ROADMAP.md b/ROADMAP.md index 13938ef..d78decd 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -30,6 +30,9 @@ Living plan for product quality, auth/email reliability, and automation. - [x] **CI: `actions/upload-artifact@v4` pinned to `v3`** — v4 doesn't work against this Gitea/act runner's artifact backend; report upload on e2e failure was silently broken - [x] **CI: npm cache corruption retry** — `viewer-unit`/`admin-unit`/`e2e` all retry `npm ci` once after `npm cache clean --force` on first failure (shared act_runner cache has corrupted `@next/swc-linux-x64-musl` before, redding CI with no product bug) - [x] ROADMAP (this file) +- [x] **Person contact fields** — optional email/phone on identify (viewer + admin), pending approve UI, Modify person +- [x] **Approve Identified bulk select** — Select All Approve/Deny + Approve Next 10 +- [x] **Admin pending-queue digest email** — `ADMIN_NOTIFY_EMAIL` via `noreply@levkine.ca` SMTP (throttled) ## Next (near-term) diff --git a/admin-frontend/src/api/faces.ts b/admin-frontend/src/api/faces.ts index af9bcf0..09d4a4d 100644 --- a/admin-frontend/src/api/faces.ts +++ b/admin-frontend/src/api/faces.ts @@ -85,6 +85,8 @@ export interface IdentifyFaceRequest { middle_name?: string maiden_name?: string date_of_birth?: string + email?: string + phone?: string additional_face_ids?: number[] } diff --git a/admin-frontend/src/api/pendingIdentifications.ts b/admin-frontend/src/api/pendingIdentifications.ts index 2109dfa..3cf68f7 100644 --- a/admin-frontend/src/api/pendingIdentifications.ts +++ b/admin-frontend/src/api/pendingIdentifications.ts @@ -12,6 +12,8 @@ export interface PendingIdentification { middle_name?: string | null maiden_name?: string | null date_of_birth?: string | null + email?: string | null + phone?: string | null status: string created_at: string updated_at: string diff --git a/admin-frontend/src/api/people.ts b/admin-frontend/src/api/people.ts index 4a0712a..aae5d0e 100644 --- a/admin-frontend/src/api/people.ts +++ b/admin-frontend/src/api/people.ts @@ -7,6 +7,8 @@ export interface Person { middle_name?: string | null maiden_name?: string | null date_of_birth?: string | null + email?: string | null + phone?: string | null } export interface PeopleListResponse { @@ -30,6 +32,8 @@ export interface PersonCreateRequest { middle_name?: string maiden_name?: string date_of_birth?: string | null + email?: string | null + phone?: string | null } export interface PersonUpdateRequest { @@ -38,6 +42,8 @@ export interface PersonUpdateRequest { middle_name?: string maiden_name?: string date_of_birth?: string | null + email?: string | null + phone?: string | null } export const peopleApi = { diff --git a/admin-frontend/src/api/videos.ts b/admin-frontend/src/api/videos.ts index 3ad7bdc..e501093 100644 --- a/admin-frontend/src/api/videos.ts +++ b/admin-frontend/src/api/videos.ts @@ -50,6 +50,8 @@ export interface IdentifyVideoRequest { middle_name?: string maiden_name?: string date_of_birth?: string | null + email?: string | null + phone?: string | null } export interface IdentifyVideoResponse { diff --git a/admin-frontend/src/pages/ApproveIdentified.tsx b/admin-frontend/src/pages/ApproveIdentified.tsx index 139c15c..4aef725 100644 --- a/admin-frontend/src/pages/ApproveIdentified.tsx +++ b/admin-frontend/src/pages/ApproveIdentified.tsx @@ -90,6 +90,42 @@ export default function ApproveIdentified() { }) } + + const actionableIds = pendingIdentifications + .filter((p) => p.status !== 'approved') + .map((p) => p.id) + + const setDecisionForIds = (ids: number[], decision: 'approve' | 'deny') => { + setDecisions((prev) => { + const updated = { ...prev } + ids.forEach((id) => { + updated[id] = decision + }) + return updated + }) + } + + const handleSelectAllApprove = () => { + setDecisionForIds(actionableIds, 'approve') + } + + const handleSelectAllDeny = () => { + setDecisionForIds(actionableIds, 'deny') + } + + const handleApproveNextOnScreen = (limit = 10) => { + const ids = actionableIds.slice(0, limit) + if (ids.length === 0) { + alert('No pending identifications to approve.') + return + } + setDecisionForIds(ids, 'approve') + } + + const handleClearDecisions = () => { + setDecisions({}) + } + const handleSubmit = async () => { // Get all decisions that have been made, for pending or denied items (not approved) const decisionsList = Object.entries(decisions) @@ -267,6 +303,39 @@ export default function ApproveIdentified() { /> Include denied + {pendingIdentifications.filter((p) => p.status !== 'approved').length > 0 && ( + <> + + + + + + )}