From b6a97653153842b6acbf04f3aa1ca4d9395aec92 Mon Sep 17 00:00:00 2001 From: Tanya Date: Wed, 7 Jan 2026 12:29:17 -0500 Subject: [PATCH] chore: Update project configuration and enhance code quality This commit modifies the `.gitignore` file to exclude Python library directories while ensuring the viewer-frontend's `lib` directory is not ignored. It also updates the `package.json` to activate the virtual environment during backend tests, improving the testing process. Additionally, the CI workflow is enhanced to prevent duplicate runs for branches with open pull requests. Various components in the viewer frontend are updated to ensure consistent naming conventions and improve type safety. These changes contribute to a cleaner codebase and a more efficient development workflow. --- .gitea/workflows/ci.yml | 6 + .gitignore | 2 + docs/ARCHITECTURE.md | 6 +- package.json | 2 +- viewer-frontend/app/HomePageContent.tsx | 4 +- viewer-frontend/app/login/page.tsx | 23 +- viewer-frontend/app/page.tsx | 11 +- viewer-frontend/app/photo/[id]/page.tsx | 18 +- viewer-frontend/app/reset-password/page.tsx | 23 +- viewer-frontend/app/search/page.tsx | 11 +- viewer-frontend/app/test-images/page.tsx | 15 +- viewer-frontend/components/PhotoViewer.tsx | 14 +- .../components/PhotoViewerClient.tsx | 31 +-- .../components/TagSelectionDialog.tsx | 10 +- .../components/search/PeopleFilter.tsx | 6 +- .../components/search/TagFilter.tsx | 12 +- viewer-frontend/lib/db.ts | 44 ++++ viewer-frontend/lib/email.ts | 196 +++++++++++++++ viewer-frontend/lib/face-utils.ts | 234 ++++++++++++++++++ viewer-frontend/lib/permissions.ts | 49 ++++ viewer-frontend/lib/photo-utils.ts | 59 +++++ viewer-frontend/lib/queries.ts | 115 +++++++++ viewer-frontend/lib/serialize.ts | 233 +++++++++++++++++ viewer-frontend/lib/utils.ts | 86 +++++++ viewer-frontend/lib/video-thumbnail.ts | 166 +++++++++++++ viewer-frontend/tsconfig.json | 2 +- 26 files changed, 1311 insertions(+), 67 deletions(-) create mode 100644 viewer-frontend/lib/db.ts create mode 100644 viewer-frontend/lib/email.ts create mode 100644 viewer-frontend/lib/face-utils.ts create mode 100644 viewer-frontend/lib/permissions.ts create mode 100644 viewer-frontend/lib/photo-utils.ts create mode 100644 viewer-frontend/lib/queries.ts create mode 100644 viewer-frontend/lib/serialize.ts create mode 100644 viewer-frontend/lib/utils.ts create mode 100644 viewer-frontend/lib/video-thumbnail.ts diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 5614c25..d92ade4 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -7,6 +7,12 @@ on: pull_request: types: [opened, synchronize, reopened] +# Prevent duplicate runs when pushing to a branch with an open PR +# This ensures only one workflow runs at a time for the same branch/PR +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + jobs: # Check if CI should be skipped based on branch name or commit message skip-ci-check: diff --git a/.gitignore b/.gitignore index 268717c..7b7cfb7 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,9 @@ dist/ downloads/ eggs/ .eggs/ +# Python lib directories (but not viewer-frontend/lib/) lib/ +!viewer-frontend/lib/ lib64/ parts/ sdist/ diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 674c688..1b3b7af 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -74,7 +74,7 @@ PunimTag is a modern web-based photo management and tagging application with adv │ │ • /api/v1/users • /api/v1/videos │ │ │ └──────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ - ↕ + ↕ ┌─────────────────────────────────────────────────────────────────────────┐ │ BUSINESS LOGIC LAYER │ │ ┌──────────────────┬──────────────────┬──────────────────────────┐ │ @@ -92,7 +92,7 @@ PunimTag is a modern web-based photo management and tagging application with adv │ │ (Multi-criteria)│ (Video Processing)│ (JWT, RBAC) │ │ │ └──────────────────┴──────────────────┴──────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ - ↕ + ↕ ┌─────────────────────────────────────────────────────────────────────────┐ │ DATA ACCESS LAYER │ │ ┌──────────────────────────────────────────────────────────────────┐ │ @@ -103,7 +103,7 @@ PunimTag is a modern web-based photo management and tagging application with adv │ │ • Query optimization • Data integrity │ │ │ └──────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ - ↕ + ↕ ┌─────────────────────────────────────────────────────────────────────────┐ │ PERSISTENCE LAYER │ │ ┌──────────────────────────────┬──────────────────────────────────┐ │ diff --git a/package.json b/package.json index e398a00..92da120 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "type-check:viewer": "npm run type-check --prefix viewer-frontend", "lint:python": "flake8 backend --max-line-length=100 --ignore=E501,W503 || true", "lint:python:syntax": "find backend -name '*.py' -exec python -m py_compile {} \\;", - "test:backend": "export PYTHONPATH=$(pwd) && python -m pytest tests/ -v", + "test:backend": "export PYTHONPATH=$(pwd) && source venv/bin/activate && python3 -m pytest tests/ -v || python3 -m pytest tests/ -v", "test:all": "npm run test:backend", "ci:local": "npm run lint:all && npm run type-check:viewer && npm run lint:python && npm run test:backend && npm run build:all", "deploy:dev": "npm run build:all && echo '✅ Build complete. Ready for deployment to dev server (10.0.10.121)'", diff --git a/viewer-frontend/app/HomePageContent.tsx b/viewer-frontend/app/HomePageContent.tsx index 3a19e3b..23c8bc6 100644 --- a/viewer-frontend/app/HomePageContent.tsx +++ b/viewer-frontend/app/HomePageContent.tsx @@ -248,9 +248,9 @@ export function HomePageContent({ initialPhotos, people, tags }: HomePageContent // Photo is already loaded, use it directly - no database access! console.log('[HomePageContent] Using existing photo from photos array:', { photoId: existingPhoto.id, - hasFaces: !!existingPhoto.faces, + hasFaces: !!(existingPhoto as any).faces, hasFace: !!(existingPhoto as any).Face, - facesCount: existingPhoto.faces?.length || (existingPhoto as any).Face?.length || 0, + facesCount: (existingPhoto as any).faces?.length || (existingPhoto as any).Face?.length || 0, photoKeys: Object.keys(existingPhoto), }); setModalPhoto(existingPhoto); diff --git a/viewer-frontend/app/login/page.tsx b/viewer-frontend/app/login/page.tsx index ec8195c..e9e605b 100644 --- a/viewer-frontend/app/login/page.tsx +++ b/viewer-frontend/app/login/page.tsx @@ -1,13 +1,13 @@ 'use client'; -import { useState } from 'react'; +import { useState, Suspense } from 'react'; import { signIn } from 'next-auth/react'; import { useRouter, useSearchParams } from 'next/navigation'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import Link from 'next/link'; -export default function LoginPage() { +function LoginForm() { const router = useRouter(); const searchParams = useSearchParams(); const callbackUrl = searchParams.get('callbackUrl') || '/'; @@ -213,3 +213,22 @@ export default function LoginPage() { ); } +export default function LoginPage() { + return ( + +
+
+

+ Sign in to your account +

+

Loading...

+
+
+ + }> + +
+ ); +} + diff --git a/viewer-frontend/app/page.tsx b/viewer-frontend/app/page.tsx index 15134ac..bf43768 100644 --- a/viewer-frontend/app/page.tsx +++ b/viewer-frontend/app/page.tsx @@ -1,3 +1,4 @@ +import { Suspense } from 'react'; import { prisma } from '@/lib/db'; import { HomePageContent } from './HomePageContent'; import { Photo } from '@prisma/client'; @@ -229,7 +230,15 @@ export default async function HomePage() {

) : ( - + +
+

Loading...

+
+ + }> + +
)} ); diff --git a/viewer-frontend/app/photo/[id]/page.tsx b/viewer-frontend/app/photo/[id]/page.tsx index 1fffb17..7bbe307 100644 --- a/viewer-frontend/app/photo/[id]/page.tsx +++ b/viewer-frontend/app/photo/[id]/page.tsx @@ -8,14 +8,14 @@ async function getPhoto(id: number) { const photo = await prisma.photo.findUnique({ where: { id }, include: { - faces: { + Face: { include: { - person: true, + Person: true, }, }, - photoTags: { + PhotoTagLinkage: { include: { - tag: true, + Tag: true, }, }, }, @@ -36,18 +36,18 @@ async function getPhotosByIds(ids: number[]) { processed: true, }, include: { - faces: { + Face: { include: { - person: true, + Person: true, }, }, - photoTags: { + PhotoTagLinkage: { include: { - tag: true, + Tag: true, }, }, }, - orderBy: { dateTaken: 'desc' }, + orderBy: { date_taken: 'desc' }, }); return serializePhotos(photos); diff --git a/viewer-frontend/app/reset-password/page.tsx b/viewer-frontend/app/reset-password/page.tsx index dccaeaf..65b39da 100644 --- a/viewer-frontend/app/reset-password/page.tsx +++ b/viewer-frontend/app/reset-password/page.tsx @@ -1,12 +1,12 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, Suspense } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import Link from 'next/link'; -export default function ResetPasswordPage() { +function ResetPasswordForm() { const router = useRouter(); const searchParams = useSearchParams(); const token = searchParams.get('token'); @@ -177,6 +177,25 @@ export default function ResetPasswordPage() { ); } +export default function ResetPasswordPage() { + return ( + +
+
+

+ Reset your password +

+

Loading...

+
+
+ + }> + +
+ ); +} + diff --git a/viewer-frontend/app/search/page.tsx b/viewer-frontend/app/search/page.tsx index 93f6347..f83f392 100644 --- a/viewer-frontend/app/search/page.tsx +++ b/viewer-frontend/app/search/page.tsx @@ -25,13 +25,16 @@ async function getAllPeople() { if (error?.code === 'P2023' || error?.message?.includes('Conversion failed')) { console.warn('Corrupted person data detected, attempting fallback query'); try { - // Try with minimal fields first + // Try with minimal fields first, but include all required fields for type compatibility return await prisma.person.findMany({ select: { id: true, first_name: true, last_name: true, - // Exclude potentially corrupted optional fields + middle_name: true, + maiden_name: true, + date_of_birth: true, + created_date: true, }, orderBy: [ { first_name: 'asc' }, @@ -64,12 +67,12 @@ async function getAllTags() { if (error?.code === 'P2023' || error?.message?.includes('Conversion failed')) { console.warn('Corrupted tag data detected, attempting fallback query'); try { - // Try with minimal fields + // Try with minimal fields, but include all required fields for type compatibility return await prisma.tag.findMany({ select: { id: true, tag_name: true, - // Exclude potentially corrupted date field + created_date: true, }, orderBy: { tag_name: 'asc' }, }); diff --git a/viewer-frontend/app/test-images/page.tsx b/viewer-frontend/app/test-images/page.tsx index f4a444c..3ea50ab 100644 --- a/viewer-frontend/app/test-images/page.tsx +++ b/viewer-frontend/app/test-images/page.tsx @@ -17,10 +17,9 @@ export default function TestImagesPage() { id: 9991, path: 'https://picsum.photos/800/600?random=1', filename: 'test-direct-url-1.jpg', - dateAdded: new Date(), - dateTaken: null, + date_added: new Date(), + date_taken: null, processed: true, - file_hash: 'test-hash-1', media_type: 'image', }, // Test 2: Another direct URL @@ -28,10 +27,9 @@ export default function TestImagesPage() { id: 9992, path: 'https://picsum.photos/800/600?random=2', filename: 'test-direct-url-2.jpg', - dateAdded: new Date(), - dateTaken: null, + date_added: new Date(), + date_taken: null, processed: true, - file_hash: 'test-hash-2', media_type: 'image', }, // Test 3: File system path (will use API proxy) @@ -39,10 +37,9 @@ export default function TestImagesPage() { id: 9993, path: '/nonexistent/path/test.jpg', filename: 'test-file-system.jpg', - dateAdded: new Date(), - dateTaken: null, + date_added: new Date(), + date_taken: null, processed: true, - file_hash: 'test-hash-3', media_type: 'image', }, ]; diff --git a/viewer-frontend/components/PhotoViewer.tsx b/viewer-frontend/components/PhotoViewer.tsx index adb4e67..6c19370 100644 --- a/viewer-frontend/components/PhotoViewer.tsx +++ b/viewer-frontend/components/PhotoViewer.tsx @@ -72,12 +72,12 @@ export function PhotoViewer({ photo, previousId, nextId }: PhotoViewerProps) { router.back(); }; - const peopleNames = photo.faces - ?.map((face) => face.person) - .filter((person): person is Person => person !== null) - .map((person) => `${person.firstName} ${person.lastName}`.trim()) || []; + const peopleNames = (photo as any).faces + ?.map((face: any) => face.Person) + .filter((person: any): person is Person => person !== null) + .map((person: Person) => `${person.first_name} ${person.last_name}`.trim()) || []; - const tags = photo.photoTags?.map((pt) => pt.tag.tagName) || []; + const tags = (photo as any).PhotoTagLinkage?.map((pt: any) => pt.Tag.tag_name) || []; return (
@@ -143,9 +143,9 @@ export function PhotoViewer({ photo, previousId, nextId }: PhotoViewerProps) {

{photo.filename}

- {photo.dateTaken && ( + {photo.date_taken && (

- {new Date(photo.dateTaken).toLocaleDateString('en-US', { + {new Date(photo.date_taken).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', diff --git a/viewer-frontend/components/PhotoViewerClient.tsx b/viewer-frontend/components/PhotoViewerClient.tsx index 90571a7..e853444 100644 --- a/viewer-frontend/components/PhotoViewerClient.tsx +++ b/viewer-frontend/components/PhotoViewerClient.tsx @@ -758,9 +758,10 @@ export function PhotoViewerClient({ const face = findFaceAtPoint(e.clientX, e.clientY); if (face) { - const personName = face.person - ? `${face.person.firstName} ${face.person.lastName}`.trim() - : null; + const person = face.person as any; + const firstName = person?.first_name || person?.firstName; + const lastName = person?.last_name || person?.lastName; + const personName = firstName && lastName ? `${firstName} ${lastName}`.trim() : null; console.log('[PhotoViewerClient] handleMouseMove: Face detected on hover', { faceId: face.id, @@ -1072,12 +1073,12 @@ export function PhotoViewerClient({ } }; - const peopleNames = currentPhoto.faces - ?.map((face) => face.person) - .filter((person): person is Person => person !== null) - .map((person) => `${person.firstName} ${person.lastName}`.trim()) || []; + const peopleNames = (currentPhoto as any).faces + ?.map((face: any) => face.Person) + .filter((person: any): person is Person => person !== null) + .map((person: Person) => `${person.first_name} ${person.last_name}`.trim()) || []; - const tags = currentPhoto.photoTags?.map((pt) => pt.tag.tagName) || []; + const tags = (currentPhoto as any).PhotoTagLinkage?.map((pt: any) => pt.Tag.tag_name) || []; const hasPrevious = allPhotos.length > 0 && currentIdx > 0; const hasNext = allPhotos.length > 0 && currentIdx < allPhotos.length - 1; @@ -1481,9 +1482,9 @@ export function PhotoViewerClient({ >

{currentPhoto.filename}

- {currentPhoto.dateTaken && ( + {currentPhoto.date_taken && (

- {new Date(currentPhoto.dateTaken).toLocaleDateString('en-US', { + {new Date(currentPhoto.date_taken).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', @@ -1522,11 +1523,11 @@ export function PhotoViewerClient({ }} faceId={clickedFace.faceId} existingPerson={clickedFace.person ? { - firstName: clickedFace.person.firstName, - lastName: clickedFace.person.lastName, - middleName: clickedFace.person.middleName, - maidenName: clickedFace.person.maidenName, - dateOfBirth: clickedFace.person.dateOfBirth, + firstName: (clickedFace.person as any).first_name || (clickedFace.person as any).firstName, + lastName: (clickedFace.person as any).last_name || (clickedFace.person as any).lastName, + middleName: (clickedFace.person as any).middle_name || (clickedFace.person as any).middleName, + maidenName: (clickedFace.person as any).maiden_name || (clickedFace.person as any).maidenName, + dateOfBirth: (clickedFace.person as any).date_of_birth || (clickedFace.person as any).dateOfBirth, } : null} onSave={handleSaveFace} /> diff --git a/viewer-frontend/components/TagSelectionDialog.tsx b/viewer-frontend/components/TagSelectionDialog.tsx index ea45fe3..afaa016 100644 --- a/viewer-frontend/components/TagSelectionDialog.tsx +++ b/viewer-frontend/components/TagSelectionDialog.tsx @@ -44,7 +44,7 @@ export function TagSelectionDialog({ return tags; } const query = searchQuery.toLowerCase(); - return tags.filter((tag) => tag.tagName.toLowerCase().includes(query)); + return tags.filter((tag) => tag.tag_name.toLowerCase().includes(query)); }, [searchQuery, tags]); useEffect(() => { @@ -93,9 +93,9 @@ export function TagSelectionDialog({ setCustomTagInput(''); }; - const removeCustomTag = (tagName: string) => { + const removeCustomTag = (tag_name: string) => { setCustomTags((prev) => - prev.filter((tag) => tag.toLowerCase() !== tagName.toLowerCase()) + prev.filter((tag) => tag.toLowerCase() !== tag_name.toLowerCase()) ); }; @@ -197,7 +197,7 @@ export function TagSelectionDialog({ checked={selectedTagIds.includes(tag.id)} onCheckedChange={() => toggleTagSelection(tag.id)} /> - {tag.tagName} + {tag.tag_name} )) )} @@ -214,7 +214,7 @@ export function TagSelectionDialog({ className="flex items-center gap-1" > - {tag.tagName} + {tag.tag_name} ); })} diff --git a/viewer-frontend/components/search/PeopleFilter.tsx b/viewer-frontend/components/search/PeopleFilter.tsx index b3dc7c4..2960a65 100644 --- a/viewer-frontend/components/search/PeopleFilter.tsx +++ b/viewer-frontend/components/search/PeopleFilter.tsx @@ -24,7 +24,7 @@ export function PeopleFilter({ people, selected, mode, onSelectionChange, onMode const [open, setOpen] = useState(false); const filteredPeople = people.filter((person) => { - const fullName = `${person.firstName} ${person.lastName}`.toLowerCase(); + const fullName = `${person.first_name} ${person.last_name}`.toLowerCase(); return fullName.includes(searchQuery.toLowerCase()); }); @@ -92,7 +92,7 @@ export function PeopleFilter({ people, selected, mode, onSelectionChange, onMode />

); @@ -111,7 +111,7 @@ export function PeopleFilter({ people, selected, mode, onSelectionChange, onMode variant="secondary" className="flex items-center gap-1" > - {person.firstName} {person.lastName} + {person.first_name} {person.last_name}
); @@ -110,7 +116,7 @@ export function TagFilter({ tags, selected, mode, onSelectionChange, onModeChang variant="secondary" className="flex items-center gap-1" > - {tag.tagName || tag.tag_name || 'Unnamed Tag'} + {getTagName(tag) || 'Unnamed Tag'}