diff --git a/admin-frontend/serve.sh b/admin-frontend/serve.sh index 006a06f..ce81f34 100644 --- a/admin-frontend/serve.sh +++ b/admin-frontend/serve.sh @@ -1,3 +1,3 @@ #!/bin/bash cd "$(dirname "$0")" -PORT=3000 HOST=0.0.0.0 exec npx --yes serve dist \ No newline at end of file +PORT=3000 HOST=0.0.0.0 exec npx --yes serve dist --single \ No newline at end of file diff --git a/admin-frontend/src/api/photos.ts b/admin-frontend/src/api/photos.ts index b8b7a6b..3f52719 100644 --- a/admin-frontend/src/api/photos.ts +++ b/admin-frontend/src/api/photos.ts @@ -143,6 +143,18 @@ export const photosApi = { ) return data }, + + getPhotoImageBlob: async (photoId: number): Promise => { + // Fetch image as blob with authentication + const response = await apiClient.get( + `/api/v1/photos/${photoId}/image`, + { + responseType: 'blob', + } + ) + // Create object URL from blob + return URL.createObjectURL(response.data) + }, } export interface PhotoSearchResult { diff --git a/admin-frontend/src/pages/ApproveIdentified.tsx b/admin-frontend/src/pages/ApproveIdentified.tsx index 913526b..139c15c 100644 --- a/admin-frontend/src/pages/ApproveIdentified.tsx +++ b/admin-frontend/src/pages/ApproveIdentified.tsx @@ -334,7 +334,7 @@ export default function ApproveIdentified() { title="Click to open full photo" > {`Face ) : ( {`Face Reference face @@ -875,7 +875,7 @@ export default function AutoMatch() { title="Click to open full photo" > Match face diff --git a/admin-frontend/src/pages/Modify.tsx b/admin-frontend/src/pages/Modify.tsx index 3bc33a2..0fc9ee7 100644 --- a/admin-frontend/src/pages/Modify.tsx +++ b/admin-frontend/src/pages/Modify.tsx @@ -2,6 +2,7 @@ import { useEffect, useState, useRef, useCallback } from 'react' import peopleApi, { PersonWithFaces, PersonFaceItem, PersonVideoItem, PersonUpdateRequest } from '../api/people' import facesApi from '../api/faces' import videosApi from '../api/videos' +import { apiClient } from '../api/client' interface EditDialogProps { person: PersonWithFaces @@ -879,12 +880,12 @@ export default function Modify() {
{`Face { // Open photo in new window - window.open(`/api/v1/photos/${face.photo_id}/image`, '_blank') + window.open(`${apiClient.defaults.baseURL}/api/v1/photos/${face.photo_id}/image`, '_blank') }} title="Click to show original photo" onError={(e) => { diff --git a/admin-frontend/src/pages/ReportedPhotos.tsx b/admin-frontend/src/pages/ReportedPhotos.tsx index fd64f75..9a0831a 100644 --- a/admin-frontend/src/pages/ReportedPhotos.tsx +++ b/admin-frontend/src/pages/ReportedPhotos.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useCallback } from 'react' +import { useEffect, useState, useCallback, useRef } from 'react' import { reportedPhotosApi, ReportedPhotoResponse, @@ -18,6 +18,8 @@ export default function ReportedPhotos() { const [submitting, setSubmitting] = useState(false) const [clearing, setClearing] = useState(false) const [statusFilter, setStatusFilter] = useState('pending') + const [imageUrls, setImageUrls] = useState>({}) + const imageUrlsRef = useRef>({}) const loadReportedPhotos = useCallback(async () => { setLoading(true) @@ -36,6 +38,18 @@ export default function ReportedPhotos() { } }) setReviewNotes(existingNotes) + + // Create direct backend URLs for images (only for non-video photos) + const newImageUrls: Record = {} + const baseURL = apiClient.defaults.baseURL || 'http://10.0.10.121:8000' + response.items.forEach((reported) => { + if (reported.photo_id && reported.photo_media_type !== 'video') { + // Use direct backend URL - the backend endpoint doesn't require auth for images + newImageUrls[reported.photo_id] = `${baseURL}/api/v1/photos/${reported.photo_id}/image` + } + }) + setImageUrls(newImageUrls) + imageUrlsRef.current = newImageUrls } catch (err: any) { setError(err.response?.data?.detail || err.message || 'Failed to load reported photos') console.error('Error loading reported photos:', err) @@ -43,6 +57,15 @@ export default function ReportedPhotos() { setLoading(false) } }, [statusFilter]) + + // Cleanup blob URLs on unmount + useEffect(() => { + return () => { + Object.values(imageUrlsRef.current).forEach((url) => { + URL.revokeObjectURL(url) + }) + } + }, []) useEffect(() => { loadReportedPhotos() @@ -364,9 +387,10 @@ export default function ReportedPhotos() { } }} /> - ) : ( + ) : imageUrls[reported.photo_id] ? ( {`Photo + ) : ( +
+ Loading... +
)}
) : ( diff --git a/admin-frontend/src/pages/Tags.tsx b/admin-frontend/src/pages/Tags.tsx index 73af192..f139608 100644 --- a/admin-frontend/src/pages/Tags.tsx +++ b/admin-frontend/src/pages/Tags.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect, useMemo, useRef } from 'react' import tagsApi, { PhotoWithTagsItem, TagResponse } from '../api/tags' import { useDeveloperMode } from '../context/DeveloperModeContext' +import { apiClient } from '../api/client' type ViewMode = 'list' | 'icons' | 'compact' @@ -753,7 +754,7 @@ export default function Tags() { {photo.id} {folder.photos.map(photo => { - const photoUrl = `/api/v1/photos/${photo.id}/image` + const photoUrl = `${apiClient.defaults.baseURL}/api/v1/photos/${photo.id}/image` const isSelected = selectedPhotoIds.has(photo.id) return ( diff --git a/admin-frontend/src/pages/UserTaggedPhotos.tsx b/admin-frontend/src/pages/UserTaggedPhotos.tsx index 0dd34ca..83fd100 100644 --- a/admin-frontend/src/pages/UserTaggedPhotos.tsx +++ b/admin-frontend/src/pages/UserTaggedPhotos.tsx @@ -469,7 +469,7 @@ export default function UserTaggedPhotos() { /> ) : ( {`Photo 0) { pendingConditions.push({ - photo_id: { in: photoIds }, - tag_id: { in: tagIds }, + photoId: { in: photoIds }, + tagId: { in: tagIds }, }); } normalizedNewNames.forEach((name) => { pendingConditions.push({ - photo_id: { in: photoIds }, - tag_name: name, + photoId: { in: photoIds }, + tagName: name, }); }); diff --git a/viewer-frontend/components/TagSelectionDialog.tsx b/viewer-frontend/components/TagSelectionDialog.tsx index afaa016..f0f5352 100644 --- a/viewer-frontend/components/TagSelectionDialog.tsx +++ b/viewer-frontend/components/TagSelectionDialog.tsx @@ -1,7 +1,6 @@ 'use client'; import { useEffect, useMemo, useState } from 'react'; -import { Tag as TagModel } from '@prisma/client'; import { Dialog, DialogContent, @@ -16,11 +15,18 @@ import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; import { Loader2, Tag as TagIcon, X } from 'lucide-react'; +interface Tag { + id: number; + tagName?: string; + tag_name?: string; + created_date?: Date | string | null; +} + interface TagSelectionDialogProps { open: boolean; onOpenChange: (open: boolean) => void; photoIds: number[]; - tags: TagModel[]; + tags: Tag[]; onSuccess?: () => void; } @@ -44,7 +50,7 @@ export function TagSelectionDialog({ return tags; } const query = searchQuery.toLowerCase(); - return tags.filter((tag) => tag.tag_name.toLowerCase().includes(query)); + return tags.filter((tag) => (tag.tagName || tag.tag_name || '').toLowerCase().includes(query)); }, [searchQuery, tags]); useEffect(() => { @@ -197,7 +203,7 @@ export function TagSelectionDialog({ checked={selectedTagIds.includes(tag.id)} onCheckedChange={() => toggleTagSelection(tag.id)} /> - {tag.tag_name} + {tag.tagName || tag.tag_name} )) )} @@ -214,7 +220,7 @@ export function TagSelectionDialog({ className="flex items-center gap-1" > - {tag.tag_name} + {tag.tagName || tag.tag_name} ); })}