generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } enum MediaType { IMAGE VIDEO AUDIO OTHER } enum PlacementMode { RANKING // Position determined by likes ranking FIXED_POSITION // Position manually set by admin FIXED_SIZE_RANKING // Position from ranking, but size is fixed FIXED_POSITION_ADJUSTABLE // Position and size set by admin } model MediaItem { id Int @id @default(autoincrement()) relativePath String @unique // Path relative to MEDIA_ROOT, e.g. "Fotobox/001.jpg" filename String // Just the filename, e.g. "001.jpg" directory String // Parent directory, e.g. "Fotobox" mediaType MediaType @default(IMAGE) mimeType String @default("application/octet-stream") fileSize BigInt @default(0) width Int? // Pixels, for images and videos height Int? // Pixels, for images and videos duration Float? // Seconds, for video and audio // Publication published Boolean @default(false) // Layout / Placement featured Boolean @default(false) // Prominent/title image placementMode PlacementMode @default(RANKING) gridColumn Int? // For fixed position (grid column start) gridRow Int? // For fixed position (grid row start) gridWidth Int @default(1) // Width in grid units gridHeight Int @default(1) // Height in grid units sortOrder Int @default(0) // Manual sort order / ranking tiebreak // Processing status thumbnailReady Boolean @default(false) previewReady Boolean @default(false) // File tracking fileHash String? // For detecting file changes lastModified DateTime? // File's mtime from filesystem lastScanned DateTime @default(now()) // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations likes Like[] @@index([published]) @@index([directory]) @@index([mediaType]) @@index([published, sortOrder]) @@index([published, placementMode]) } model Like { id Int @id @default(autoincrement()) mediaItemId Int ipHash String // SHA-256 hash of IP address fingerprint String? // Optional browser fingerprint hash createdAt DateTime @default(now()) mediaItem MediaItem @relation(fields: [mediaItemId], references: [id], onDelete: Cascade) @@unique([mediaItemId, ipHash]) // One like per IP per media item @@index([ipHash]) @@index([mediaItemId]) @@index([createdAt]) } model AppSettings { id Int @id @default(1) votingOpen Boolean @default(false) zipVersion Int @default(0) zipReady Boolean @default(false) zipBuildAt DateTime? zipFilePath String? // Path to current valid ZIP lastScanAt DateTime? updatedAt DateTime @updatedAt } model AdminSession { id String @id @default(uuid()) ipAddress String userAgent String @default("") createdAt DateTime @default(now()) expiresAt DateTime @@index([expiresAt]) } model RateLimitEntry { id Int @id @default(autoincrement()) ipHash String action String // e.g. "login", "like" windowStart DateTime count Int @default(1) @@unique([ipHash, action, windowStart]) @@index([ipHash, action, windowStart]) @@index([windowStart]) // For cleanup of old entries }