This commit introduces several new scripts for managing database operations, including user creation, permission grants, and data migrations. It also adds new documentation files to guide users through the setup and configuration processes. Additionally, the project structure is updated to enhance organization and maintainability, ensuring a smoother development experience for contributors. These changes support the ongoing transition to a web-based architecture and improve overall project functionality.
237 lines
7.1 KiB
TypeScript
237 lines
7.1 KiB
TypeScript
import { prisma } from '@/lib/db';
|
|
import { HomePageContent } from './HomePageContent';
|
|
import { Photo } from '@prisma/client';
|
|
import { serializePhotos, serializePeople, serializeTags } from '@/lib/serialize';
|
|
|
|
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>
|
|
) : (
|
|
<HomePageContent initialPhotos={serializePhotos(photos)} people={serializePeople(people)} tags={serializeTags(tags)} />
|
|
)}
|
|
</main>
|
|
);
|
|
}
|