Some checks failed
CI / skip-ci-check (push) Successful in 1m27s
CI / skip-ci-check (pull_request) Successful in 1m27s
CI / python-lint (push) Has been cancelled
CI / test-backend (push) Has been cancelled
CI / build (push) Has been cancelled
CI / secret-scanning (push) Has been cancelled
CI / dependency-scan (push) Has been cancelled
CI / sast-scan (push) Has been cancelled
CI / workflow-summary (push) Has been cancelled
CI / lint-and-type-check (push) Has been cancelled
CI / lint-and-type-check (pull_request) Successful in 2m6s
CI / python-lint (pull_request) Successful in 1m54s
CI / test-backend (pull_request) Successful in 2m38s
CI / build (pull_request) Successful in 2m24s
CI / secret-scanning (pull_request) Successful in 1m40s
CI / dependency-scan (pull_request) Successful in 1m33s
CI / sast-scan (pull_request) Successful in 2m44s
CI / workflow-summary (pull_request) Successful in 1m26s
This commit adds dynamic rendering to the main page, photo detail page, and search page in the viewer frontend. By enforcing dynamic rendering, we prevent database queries during the build process, enhancing application performance and reliability. These changes contribute to a more efficient development workflow and improve the overall user experience.
249 lines
7.5 KiB
TypeScript
249 lines
7.5 KiB
TypeScript
import { Suspense } from 'react';
|
|
import { prisma } from '@/lib/db';
|
|
import { HomePageContent } from './HomePageContent';
|
|
import { Photo } from '@prisma/client';
|
|
import { serializePhotos, serializePeople, serializeTags } from '@/lib/serialize';
|
|
|
|
// Force dynamic rendering to prevent database queries during build
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
async function getAllPeople() {
|
|
try {
|
|
return await prisma.person.findMany({
|
|
select: {
|
|
id: true,
|
|
first_name: true,
|
|
last_name: true,
|
|
middle_name: true,
|
|
maiden_name: true,
|
|
date_of_birth: true,
|
|
created_date: true,
|
|
},
|
|
orderBy: [
|
|
{ first_name: 'asc' },
|
|
{ last_name: 'asc' },
|
|
],
|
|
});
|
|
} catch (error: any) {
|
|
// Handle corrupted data errors (P2023)
|
|
if (error?.code === 'P2023' || error?.message?.includes('Conversion failed')) {
|
|
console.warn('Corrupted person data detected, attempting fallback query');
|
|
try {
|
|
// Try with minimal fields first
|
|
return await prisma.person.findMany({
|
|
select: {
|
|
id: true,
|
|
first_name: true,
|
|
last_name: true,
|
|
// Exclude potentially corrupted optional fields
|
|
},
|
|
orderBy: [
|
|
{ first_name: 'asc' },
|
|
{ last_name: 'asc' },
|
|
],
|
|
});
|
|
} catch (fallbackError: any) {
|
|
console.error('Fallback person query also failed:', fallbackError);
|
|
// Return empty array as last resort to prevent page crash
|
|
return [];
|
|
}
|
|
}
|
|
// Re-throw if it's a different error
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function getAllTags() {
|
|
try {
|
|
return await prisma.tag.findMany({
|
|
select: {
|
|
id: true,
|
|
tag_name: true,
|
|
created_date: true,
|
|
},
|
|
orderBy: { tag_name: 'asc' },
|
|
});
|
|
} catch (error: any) {
|
|
// Handle corrupted data errors (P2023)
|
|
if (error?.code === 'P2023' || error?.message?.includes('Conversion failed')) {
|
|
console.warn('Corrupted tag data detected, attempting fallback query');
|
|
try {
|
|
// Try with minimal fields
|
|
return await prisma.tag.findMany({
|
|
select: {
|
|
id: true,
|
|
tag_name: true,
|
|
// Exclude potentially corrupted date field
|
|
},
|
|
orderBy: { tag_name: 'asc' },
|
|
});
|
|
} catch (fallbackError: any) {
|
|
console.error('Fallback tag query also failed:', fallbackError);
|
|
// Return empty array as last resort to prevent page crash
|
|
return [];
|
|
}
|
|
}
|
|
// Re-throw if it's a different error
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export default async function HomePage() {
|
|
// Fetch photos from database
|
|
// Note: Make sure DATABASE_URL is set in .env file
|
|
let photos: any[] = []; // Using any to handle select-based query return type
|
|
let error: string | null = null;
|
|
|
|
try {
|
|
// Fetch first page of photos (30 photos) for initial load
|
|
// Infinite scroll will load more as user scrolls
|
|
// Try to load with date fields first, fallback if corrupted data exists
|
|
let photosBase;
|
|
try {
|
|
// Use raw query to read dates as strings and convert manually to avoid Prisma conversion issues
|
|
const photosRaw = await prisma.$queryRaw<Array<{
|
|
id: number;
|
|
path: string;
|
|
filename: string;
|
|
date_added: string;
|
|
date_taken: string | null;
|
|
processed: boolean;
|
|
media_type: string | null;
|
|
}>>`
|
|
SELECT
|
|
id,
|
|
path,
|
|
filename,
|
|
date_added,
|
|
date_taken,
|
|
processed,
|
|
media_type
|
|
FROM photos
|
|
WHERE processed = true
|
|
ORDER BY date_taken DESC, id DESC
|
|
LIMIT 30
|
|
`;
|
|
|
|
photosBase = photosRaw.map(photo => ({
|
|
id: photo.id,
|
|
path: photo.path,
|
|
filename: photo.filename,
|
|
date_added: new Date(photo.date_added),
|
|
date_taken: photo.date_taken ? new Date(photo.date_taken) : null,
|
|
processed: photo.processed,
|
|
media_type: photo.media_type,
|
|
}));
|
|
} catch (dateError: any) {
|
|
// If date fields are corrupted, load without them and use fallback values
|
|
// Check for P2023 error code or various date conversion error messages
|
|
const isDateError = dateError?.code === 'P2023' ||
|
|
dateError?.message?.includes('Conversion failed') ||
|
|
dateError?.message?.includes('Inconsistent column data') ||
|
|
dateError?.message?.includes('Could not convert value');
|
|
|
|
if (isDateError) {
|
|
console.warn('Corrupted date data detected, loading photos without date fields');
|
|
photosBase = await prisma.photo.findMany({
|
|
where: { processed: true },
|
|
select: {
|
|
id: true,
|
|
path: true,
|
|
filename: true,
|
|
processed: true,
|
|
media_type: true,
|
|
// Exclude date fields due to corruption
|
|
},
|
|
orderBy: { id: 'desc' },
|
|
take: 30,
|
|
});
|
|
// Add fallback date values
|
|
photosBase = photosBase.map(photo => ({
|
|
...photo,
|
|
date_added: new Date(),
|
|
date_taken: null,
|
|
}));
|
|
} else {
|
|
throw dateError;
|
|
}
|
|
}
|
|
|
|
// If base query works, load faces separately
|
|
const photoIds = photosBase.map(p => p.id);
|
|
const faces = await prisma.face.findMany({
|
|
where: { photo_id: { in: photoIds } },
|
|
select: {
|
|
id: true,
|
|
photo_id: true,
|
|
person_id: true,
|
|
location: true,
|
|
confidence: true,
|
|
quality_score: true,
|
|
is_primary_encoding: true,
|
|
detector_backend: true,
|
|
model_name: true,
|
|
face_confidence: true,
|
|
exif_orientation: true,
|
|
pose_mode: true,
|
|
yaw_angle: true,
|
|
pitch_angle: true,
|
|
roll_angle: true,
|
|
landmarks: true,
|
|
identified_by_user_id: true,
|
|
excluded: true,
|
|
Person: {
|
|
select: {
|
|
id: true,
|
|
first_name: true,
|
|
last_name: true,
|
|
middle_name: true,
|
|
maiden_name: true,
|
|
date_of_birth: true,
|
|
created_date: true,
|
|
},
|
|
},
|
|
// Exclude encoding field (Bytes) to avoid P2023 conversion errors
|
|
},
|
|
});
|
|
|
|
// Combine the data manually
|
|
photos = photosBase.map(photo => ({
|
|
...photo,
|
|
Face: faces.filter(face => face.photo_id === photo.id),
|
|
})) as any;
|
|
} catch (err) {
|
|
error = err instanceof Error ? err.message : 'Failed to load photos';
|
|
console.error('Error loading photos:', err);
|
|
}
|
|
|
|
// Fetch people and tags for search
|
|
const [people, tags] = await Promise.all([
|
|
getAllPeople(),
|
|
getAllTags(),
|
|
]);
|
|
|
|
return (
|
|
<main className="w-full px-4 py-8">
|
|
|
|
{error ? (
|
|
<div className="rounded-lg bg-red-50 p-4 text-red-800 dark:bg-red-900/20 dark:text-red-200">
|
|
<p className="font-semibold">Error loading photos</p>
|
|
<p className="text-sm">{error}</p>
|
|
<p className="mt-2 text-xs">
|
|
Make sure DATABASE_URL is configured in your .env file
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<Suspense fallback={
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="text-center">
|
|
<p className="text-gray-600 dark:text-gray-400">Loading...</p>
|
|
</div>
|
|
</div>
|
|
}>
|
|
<HomePageContent initialPhotos={serializePhotos(photos)} people={serializePeople(people)} tags={serializeTags(tags)} />
|
|
</Suspense>
|
|
)}
|
|
</main>
|
|
);
|
|
}
|