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 (
+ Loading...
+ Sign in to your account
+
+
Loading...
+Loading...
+- {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({ >
- {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"
>