feat: extend people search to first/middle/last/maiden names and fix port binding issue
- Backend: Updated list_people_with_faces to search by first_name, middle_name, last_name, and maiden_name - Frontend: Updated Modify page search UI and API client to support extended search - Frontend: Updated Help page documentation for new search capabilities - Infrastructure: Added start-api.sh wrapper script to prevent port 8000 binding conflicts - Infrastructure: Updated PM2 config with improved kill_timeout and restart_delay settings
This commit is contained in:
parent
041d3728a1
commit
09ee8712aa
@ -46,8 +46,8 @@ export const peopleApi = {
|
||||
const res = await apiClient.get<PeopleListResponse>('/api/v1/people', { params })
|
||||
return res.data
|
||||
},
|
||||
listWithFaces: async (lastName?: string): Promise<PeopleWithFacesListResponse> => {
|
||||
const params = lastName ? { last_name: lastName } : {}
|
||||
listWithFaces: async (name?: string): Promise<PeopleWithFacesListResponse> => {
|
||||
const params = name ? { last_name: name } : {}
|
||||
const res = await apiClient.get<PeopleWithFacesListResponse>('/api/v1/people/with-faces', { params })
|
||||
return res.data
|
||||
},
|
||||
|
||||
@ -672,7 +672,7 @@ function ModifyPageHelp({ onBack }: { onBack: () => void }) {
|
||||
<p className="text-gray-700 font-medium mb-2">Finding and Selecting a Person:</p>
|
||||
<ol className="list-decimal list-inside space-y-1 text-gray-600 ml-4">
|
||||
<li>Navigate to Modify page</li>
|
||||
<li>Optionally search for a person by entering their last name or maiden name in the search box</li>
|
||||
<li>Optionally search for a person by entering their first, middle, last, or maiden name in the search box</li>
|
||||
<li>Click "Search" to filter the list, or "Clear" to show all people</li>
|
||||
<li>Click on a person's name in the left panel to select them</li>
|
||||
<li>The person's faces and videos will load in the right panels</li>
|
||||
|
||||
@ -147,7 +147,7 @@ function EditPersonDialog({ person, onSave, onClose }: EditDialogProps) {
|
||||
|
||||
export default function Modify() {
|
||||
const [people, setPeople] = useState<PersonWithFaces[]>([])
|
||||
const [lastNameFilter, setLastNameFilter] = useState('')
|
||||
const [nameFilter, setNameFilter] = useState('')
|
||||
const [selectedPersonId, setSelectedPersonId] = useState<number | null>(null)
|
||||
const [selectedPersonName, setSelectedPersonName] = useState('')
|
||||
const [faces, setFaces] = useState<PersonFaceItem[]>([])
|
||||
@ -187,7 +187,7 @@ export default function Modify() {
|
||||
try {
|
||||
setBusy(true)
|
||||
setError(null)
|
||||
const res = await peopleApi.listWithFaces(lastNameFilter || undefined)
|
||||
const res = await peopleApi.listWithFaces(nameFilter || undefined)
|
||||
setPeople(res.items)
|
||||
|
||||
// Auto-select first person if available and none selected (only if not restoring state)
|
||||
@ -203,7 +203,7 @@ export default function Modify() {
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}, [lastNameFilter, selectedPersonId])
|
||||
}, [nameFilter, selectedPersonId])
|
||||
|
||||
// Load faces for a person
|
||||
const loadPersonFaces = useCallback(async (personId: number) => {
|
||||
@ -248,12 +248,15 @@ export default function Modify() {
|
||||
useEffect(() => {
|
||||
let restoredPanelWidth = false
|
||||
try {
|
||||
const saved = sessionStorage.getItem(STATE_KEY)
|
||||
if (saved) {
|
||||
const state = JSON.parse(saved)
|
||||
if (state.lastNameFilter !== undefined) {
|
||||
setLastNameFilter(state.lastNameFilter || '')
|
||||
}
|
||||
const saved = sessionStorage.getItem(STATE_KEY)
|
||||
if (saved) {
|
||||
const state = JSON.parse(saved)
|
||||
if (state.nameFilter !== undefined) {
|
||||
setNameFilter(state.nameFilter || '')
|
||||
} else if (state.lastNameFilter !== undefined) {
|
||||
// Backward compatibility with old state key
|
||||
setNameFilter(state.lastNameFilter || '')
|
||||
}
|
||||
if (state.selectedPersonId !== undefined && state.selectedPersonId !== null) {
|
||||
setSelectedPersonId(state.selectedPersonId)
|
||||
}
|
||||
@ -365,7 +368,7 @@ export default function Modify() {
|
||||
|
||||
try {
|
||||
const state = {
|
||||
lastNameFilter,
|
||||
nameFilter,
|
||||
selectedPersonId,
|
||||
selectedPersonName,
|
||||
faces,
|
||||
@ -380,10 +383,10 @@ export default function Modify() {
|
||||
} catch (error) {
|
||||
console.error('Error saving state to sessionStorage:', error)
|
||||
}
|
||||
}, [lastNameFilter, selectedPersonId, selectedPersonName, faces, videos, selectedFaces, selectedVideos, facesExpanded, videosExpanded, peoplePanelWidth, stateRestored])
|
||||
}, [nameFilter, selectedPersonId, selectedPersonName, faces, videos, selectedFaces, selectedVideos, facesExpanded, videosExpanded, peoplePanelWidth, stateRestored])
|
||||
|
||||
// Save state on unmount (when navigating away) - use refs to capture latest values
|
||||
const lastNameFilterRef = useRef(lastNameFilter)
|
||||
const nameFilterRef = useRef(nameFilter)
|
||||
const selectedPersonIdRef = useRef(selectedPersonId)
|
||||
const selectedPersonNameRef = useRef(selectedPersonName)
|
||||
const facesRef = useRef(faces)
|
||||
@ -396,7 +399,7 @@ export default function Modify() {
|
||||
|
||||
// Update refs whenever state changes
|
||||
useEffect(() => {
|
||||
lastNameFilterRef.current = lastNameFilter
|
||||
nameFilterRef.current = nameFilter
|
||||
selectedPersonIdRef.current = selectedPersonId
|
||||
selectedPersonNameRef.current = selectedPersonName
|
||||
facesRef.current = faces
|
||||
@ -406,14 +409,14 @@ export default function Modify() {
|
||||
facesExpandedRef.current = facesExpanded
|
||||
videosExpandedRef.current = videosExpanded
|
||||
peoplePanelWidthRef.current = peoplePanelWidth
|
||||
}, [lastNameFilter, selectedPersonId, selectedPersonName, faces, videos, selectedFaces, selectedVideos, facesExpanded, videosExpanded, peoplePanelWidth])
|
||||
}, [nameFilter, selectedPersonId, selectedPersonName, faces, videos, selectedFaces, selectedVideos, facesExpanded, videosExpanded, peoplePanelWidth])
|
||||
|
||||
// Save state on unmount (when navigating away)
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
try {
|
||||
const state = {
|
||||
lastNameFilter: lastNameFilterRef.current,
|
||||
nameFilter: nameFilterRef.current,
|
||||
selectedPersonId: selectedPersonIdRef.current,
|
||||
selectedPersonName: selectedPersonNameRef.current,
|
||||
faces: facesRef.current,
|
||||
@ -463,7 +466,7 @@ export default function Modify() {
|
||||
}
|
||||
|
||||
const handleClearSearch = () => {
|
||||
setLastNameFilter('')
|
||||
setNameFilter('')
|
||||
// loadPeople will be called by useEffect
|
||||
}
|
||||
|
||||
@ -560,7 +563,7 @@ export default function Modify() {
|
||||
await facesApi.batchUnmatch({ face_ids: [unmatchConfirmDialog.faceId] })
|
||||
|
||||
// Reload people list to update face counts
|
||||
const peopleRes = await peopleApi.listWithFaces(lastNameFilter || undefined)
|
||||
const peopleRes = await peopleApi.listWithFaces(nameFilter || undefined)
|
||||
setPeople(peopleRes.items)
|
||||
|
||||
// Reload faces
|
||||
@ -591,7 +594,7 @@ export default function Modify() {
|
||||
setSelectedFaces(new Set())
|
||||
|
||||
// Reload people list to update face counts
|
||||
const peopleRes = await peopleApi.listWithFaces(lastNameFilter || undefined)
|
||||
const peopleRes = await peopleApi.listWithFaces(nameFilter || undefined)
|
||||
setPeople(peopleRes.items)
|
||||
|
||||
// Reload faces
|
||||
@ -627,7 +630,7 @@ export default function Modify() {
|
||||
await videosApi.removePerson(unmatchConfirmDialog.videoId, selectedPersonId)
|
||||
|
||||
// Reload people list
|
||||
const peopleRes = await peopleApi.listWithFaces(lastNameFilter || undefined)
|
||||
const peopleRes = await peopleApi.listWithFaces(nameFilter || undefined)
|
||||
setPeople(peopleRes.items)
|
||||
|
||||
// Reload videos
|
||||
@ -679,7 +682,7 @@ export default function Modify() {
|
||||
setSelectedVideos(new Set())
|
||||
|
||||
// Reload people list
|
||||
const peopleRes = await peopleApi.listWithFaces(lastNameFilter || undefined)
|
||||
const peopleRes = await peopleApi.listWithFaces(nameFilter || undefined)
|
||||
setPeople(peopleRes.items)
|
||||
|
||||
// Reload videos
|
||||
@ -720,10 +723,10 @@ export default function Modify() {
|
||||
<div className="flex gap-2 mb-1">
|
||||
<input
|
||||
type="text"
|
||||
value={lastNameFilter}
|
||||
onChange={(e) => setLastNameFilter(e.target.value)}
|
||||
value={nameFilter}
|
||||
onChange={(e) => setNameFilter(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
placeholder="Type Last Name or Maiden Name"
|
||||
placeholder="Type First, Middle, Last, or Maiden Name"
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<button
|
||||
@ -739,7 +742,7 @@ export default function Modify() {
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">Search by Last Name or Maiden Name</p>
|
||||
<p className="text-xs text-gray-500">Search by First, Middle, Last, or Maiden Name</p>
|
||||
</div>
|
||||
|
||||
{/* People list */}
|
||||
|
||||
@ -6,7 +6,7 @@ from datetime import datetime
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy import func, or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.db.session import get_db
|
||||
@ -48,12 +48,12 @@ def list_people(
|
||||
|
||||
@router.get("/with-faces", response_model=PeopleWithFacesListResponse)
|
||||
def list_people_with_faces(
|
||||
last_name: str | None = Query(None, description="Filter by last name or maiden name (case-insensitive)"),
|
||||
last_name: str | None = Query(None, description="Filter by first, middle, last, or maiden name (case-insensitive)"),
|
||||
db: Session = Depends(get_db),
|
||||
) -> PeopleWithFacesListResponse:
|
||||
"""List all people with face counts and video counts, sorted by last_name, first_name.
|
||||
|
||||
Optionally filter by last_name or maiden_name if provided (case-insensitive search).
|
||||
Optionally filter by first_name, middle_name, last_name, or maiden_name if provided (case-insensitive search).
|
||||
Returns all people, including those with zero faces or videos.
|
||||
"""
|
||||
# Query people with face counts using LEFT OUTER JOIN to include people with no faces
|
||||
@ -67,11 +67,15 @@ def list_people_with_faces(
|
||||
)
|
||||
|
||||
if last_name:
|
||||
# Case-insensitive search on both last_name and maiden_name
|
||||
# Case-insensitive search on first_name, middle_name, last_name, and maiden_name
|
||||
search_term = last_name.lower()
|
||||
query = query.filter(
|
||||
(func.lower(Person.last_name).contains(search_term)) |
|
||||
((Person.maiden_name.isnot(None)) & (func.lower(Person.maiden_name).contains(search_term)))
|
||||
or_(
|
||||
func.lower(Person.first_name).contains(search_term),
|
||||
func.lower(Person.middle_name).contains(search_term),
|
||||
func.lower(Person.last_name).contains(search_term),
|
||||
((Person.maiden_name.isnot(None)) & (func.lower(Person.maiden_name).contains(search_term)))
|
||||
)
|
||||
)
|
||||
|
||||
results = query.order_by(Person.last_name.asc(), Person.first_name.asc()).all()
|
||||
|
||||
13
scripts/start-api.sh
Executable file
13
scripts/start-api.sh
Executable file
@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
# Wrapper script to start the API with port cleanup
|
||||
|
||||
# Kill any processes using port 8000 (except our own if we're restarting)
|
||||
PORT=8000
|
||||
lsof -ti :${PORT} | xargs -r kill -9 2>/dev/null || true
|
||||
|
||||
# Wait a moment for port to be released
|
||||
sleep 2
|
||||
|
||||
# Start uvicorn
|
||||
exec /opt/punimtag/venv/bin/uvicorn backend.app:app --host 0.0.0.0 --port 8000
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user