mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-05-11 06:11:15 +00:00
Group merge UI: - Add mergeGroups query and mergeGroupsAction server action - Add "Start Merge" / "Merge Here" buttons to group row actions - Two-step UX: click Start on source, click Merge Here on target ZIP path prefix grouping (Signal 7): - Compare PackageFile.path root folders across ungrouped packages - Auto-group if 2+ packages share the same dominant root folder Reply chain grouping (Signal 6): - Capture reply_to_message_id during channel scanning - Group archives that reply to the same root message - Add replyToMessageId field to Package schema Caption fuzzy match grouping (Signal 8): - Capture source caption during channel scanning - Normalize captions (strip extensions, extract significant words) - Group packages with matching normalized caption keys - Add sourceCaption field to Package schema Periodic integrity audit: - Check multipart packages for completeness (parts vs destMessageIds) - Detect orphaned indexes (destChannelId set but no destMessageId) - Runs after each ingestion cycle, deduplicates notifications Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
850 lines
24 KiB
Plaintext
850 lines
24 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
}
|
|
|
|
// ───────────────────────────────────────
|
|
// Auth.js required models
|
|
// ───────────────────────────────────────
|
|
|
|
enum Role {
|
|
ADMIN
|
|
USER
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
name String?
|
|
email String? @unique
|
|
emailVerified DateTime?
|
|
image String?
|
|
hashedPassword String?
|
|
role Role @default(ADMIN)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
accounts Account[]
|
|
sessions Session[]
|
|
filaments Filament[]
|
|
resins Resin[]
|
|
paints Paint[]
|
|
supplies Supply[]
|
|
vendors Vendor[]
|
|
locations Location[]
|
|
usageLogs UsageLog[]
|
|
tags Tag[]
|
|
settings UserSettings?
|
|
telegramLink TelegramLink?
|
|
kickstarters Kickstarter[]
|
|
inviteCodes InviteCode[] @relation("InviteCreator")
|
|
usedInvite InviteCode? @relation("InviteUser", fields: [usedInviteId], references: [id], onDelete: SetNull)
|
|
usedInviteId String?
|
|
}
|
|
|
|
model Account {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
type String
|
|
provider String
|
|
providerAccountId String
|
|
refresh_token String? @db.Text
|
|
access_token String? @db.Text
|
|
expires_at Int?
|
|
token_type String?
|
|
scope String?
|
|
id_token String? @db.Text
|
|
session_state String?
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([provider, providerAccountId])
|
|
}
|
|
|
|
model Session {
|
|
id String @id @default(cuid())
|
|
sessionToken String @unique
|
|
userId String
|
|
expires DateTime
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
}
|
|
|
|
model VerificationToken {
|
|
identifier String
|
|
token String
|
|
expires DateTime
|
|
|
|
@@unique([identifier, token])
|
|
}
|
|
|
|
// ───────────────────────────────────────
|
|
// Domain models
|
|
// ───────────────────────────────────────
|
|
|
|
model Vendor {
|
|
id String @id @default(cuid())
|
|
name String @db.VarChar(64)
|
|
website String? @db.VarChar(256)
|
|
notes String? @db.Text
|
|
archived Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
userId String
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
filaments Filament[]
|
|
resins Resin[]
|
|
paints Paint[]
|
|
supplies Supply[]
|
|
|
|
@@index([userId])
|
|
@@index([archived])
|
|
}
|
|
|
|
model Location {
|
|
id String @id @default(cuid())
|
|
name String @db.VarChar(64)
|
|
description String? @db.VarChar(256)
|
|
archived Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
userId String
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
filaments Filament[]
|
|
resins Resin[]
|
|
paints Paint[]
|
|
supplies Supply[]
|
|
|
|
@@index([userId])
|
|
@@index([archived])
|
|
}
|
|
|
|
model Filament {
|
|
id String @id @default(cuid())
|
|
name String @db.VarChar(128)
|
|
brand String @db.VarChar(64)
|
|
material String @db.VarChar(32)
|
|
color String @db.VarChar(64)
|
|
colorHex String @db.VarChar(7)
|
|
diameter Float @default(1.75)
|
|
spoolWeight Float
|
|
usedWeight Float @default(0)
|
|
emptySpoolWeight Float @default(0)
|
|
purchaseDate DateTime?
|
|
cost Float?
|
|
notes String? @db.Text
|
|
archived Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
userId String
|
|
vendorId String?
|
|
locationId String?
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
vendor Vendor? @relation(fields: [vendorId], references: [id], onDelete: SetNull)
|
|
location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull)
|
|
tags TagOnFilament[]
|
|
usageLogs UsageLog[]
|
|
|
|
@@index([userId])
|
|
@@index([vendorId])
|
|
@@index([locationId])
|
|
@@index([material])
|
|
@@index([archived])
|
|
@@index([brand])
|
|
}
|
|
|
|
model Resin {
|
|
id String @id @default(cuid())
|
|
name String @db.VarChar(128)
|
|
brand String @db.VarChar(64)
|
|
resinType String @db.VarChar(32)
|
|
color String @db.VarChar(64)
|
|
colorHex String @db.VarChar(7)
|
|
bottleSize Float
|
|
usedML Float @default(0)
|
|
purchaseDate DateTime?
|
|
cost Float?
|
|
notes String? @db.Text
|
|
archived Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
userId String
|
|
vendorId String?
|
|
locationId String?
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
vendor Vendor? @relation(fields: [vendorId], references: [id], onDelete: SetNull)
|
|
location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull)
|
|
tags TagOnResin[]
|
|
usageLogs UsageLog[]
|
|
|
|
@@index([userId])
|
|
@@index([vendorId])
|
|
@@index([locationId])
|
|
@@index([resinType])
|
|
@@index([archived])
|
|
@@index([brand])
|
|
}
|
|
|
|
model Paint {
|
|
id String @id @default(cuid())
|
|
name String @db.VarChar(128)
|
|
brand String @db.VarChar(64)
|
|
line String? @db.VarChar(64)
|
|
color String @db.VarChar(64)
|
|
colorHex String @db.VarChar(7)
|
|
finish String @db.VarChar(32)
|
|
volumeML Float
|
|
usedML Float @default(0)
|
|
purchaseDate DateTime?
|
|
cost Float?
|
|
notes String? @db.Text
|
|
archived Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
userId String
|
|
vendorId String?
|
|
locationId String?
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
vendor Vendor? @relation(fields: [vendorId], references: [id], onDelete: SetNull)
|
|
location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull)
|
|
tags TagOnPaint[]
|
|
usageLogs UsageLog[]
|
|
|
|
@@index([userId])
|
|
@@index([vendorId])
|
|
@@index([locationId])
|
|
@@index([finish])
|
|
@@index([archived])
|
|
@@index([brand])
|
|
}
|
|
|
|
model Tag {
|
|
id String @id @default(cuid())
|
|
name String @db.VarChar(64)
|
|
createdAt DateTime @default(now())
|
|
userId String
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
filaments TagOnFilament[]
|
|
resins TagOnResin[]
|
|
paints TagOnPaint[]
|
|
supplies TagOnSupply[]
|
|
|
|
@@unique([name, userId])
|
|
@@index([userId])
|
|
}
|
|
|
|
model TagOnFilament {
|
|
filamentId String
|
|
tagId String
|
|
|
|
filament Filament @relation(fields: [filamentId], references: [id], onDelete: Cascade)
|
|
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
|
|
|
@@id([filamentId, tagId])
|
|
}
|
|
|
|
model TagOnResin {
|
|
resinId String
|
|
tagId String
|
|
|
|
resin Resin @relation(fields: [resinId], references: [id], onDelete: Cascade)
|
|
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
|
|
|
@@id([resinId, tagId])
|
|
}
|
|
|
|
model TagOnPaint {
|
|
paintId String
|
|
tagId String
|
|
|
|
paint Paint @relation(fields: [paintId], references: [id], onDelete: Cascade)
|
|
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
|
|
|
@@id([paintId, tagId])
|
|
}
|
|
|
|
model Supply {
|
|
id String @id @default(cuid())
|
|
name String @db.VarChar(128)
|
|
brand String @db.VarChar(64)
|
|
category String @db.VarChar(32)
|
|
color String? @db.VarChar(64)
|
|
colorHex String? @db.VarChar(7)
|
|
totalAmount Float
|
|
usedAmount Float @default(0)
|
|
unit String @db.VarChar(16)
|
|
purchaseDate DateTime?
|
|
cost Float?
|
|
notes String? @db.Text
|
|
archived Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
userId String
|
|
vendorId String?
|
|
locationId String?
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
vendor Vendor? @relation(fields: [vendorId], references: [id], onDelete: SetNull)
|
|
location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull)
|
|
tags TagOnSupply[]
|
|
usageLogs UsageLog[]
|
|
|
|
@@index([userId])
|
|
@@index([vendorId])
|
|
@@index([locationId])
|
|
@@index([category])
|
|
@@index([archived])
|
|
@@index([brand])
|
|
}
|
|
|
|
model TagOnSupply {
|
|
supplyId String
|
|
tagId String
|
|
|
|
supply Supply @relation(fields: [supplyId], references: [id], onDelete: Cascade)
|
|
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
|
|
|
@@id([supplyId, tagId])
|
|
}
|
|
|
|
model UsageLog {
|
|
id String @id @default(cuid())
|
|
itemType String @db.VarChar(16)
|
|
itemId String
|
|
amount Float
|
|
unit String @db.VarChar(16)
|
|
notes String? @db.Text
|
|
createdAt DateTime @default(now())
|
|
userId String
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
filament Filament? @relation(fields: [filamentId], references: [id], onDelete: Cascade)
|
|
filamentId String?
|
|
resin Resin? @relation(fields: [resinId], references: [id], onDelete: Cascade)
|
|
resinId String?
|
|
paint Paint? @relation(fields: [paintId], references: [id], onDelete: Cascade)
|
|
paintId String?
|
|
supply Supply? @relation(fields: [supplyId], references: [id], onDelete: Cascade)
|
|
supplyId String?
|
|
|
|
@@index([userId])
|
|
@@index([itemType, itemId])
|
|
@@index([createdAt])
|
|
}
|
|
|
|
model UserSettings {
|
|
id String @id @default(cuid())
|
|
userId String @unique
|
|
lowStockThreshold Float @default(10)
|
|
currency String @default("USD") @db.VarChar(3)
|
|
theme String @default("dark") @db.VarChar(8)
|
|
units String @default("metric") @db.VarChar(8)
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
}
|
|
|
|
// ───────────────────────────────────────
|
|
// Telegram ingestion models
|
|
// ───────────────────────────────────────
|
|
|
|
enum AuthState {
|
|
PENDING
|
|
AWAITING_CODE
|
|
AWAITING_PASSWORD
|
|
AUTHENTICATED
|
|
EXPIRED
|
|
}
|
|
|
|
enum ChannelType {
|
|
SOURCE
|
|
DESTINATION
|
|
}
|
|
|
|
enum ChannelRole {
|
|
READER
|
|
WRITER
|
|
}
|
|
|
|
enum ArchiveType {
|
|
ZIP
|
|
RAR
|
|
SEVEN_Z
|
|
DOCUMENT
|
|
}
|
|
|
|
enum IngestionStatus {
|
|
RUNNING
|
|
COMPLETED
|
|
FAILED
|
|
CANCELLED
|
|
}
|
|
|
|
enum FetchStatus {
|
|
PENDING
|
|
IN_PROGRESS
|
|
COMPLETED
|
|
FAILED
|
|
}
|
|
|
|
model TelegramAccount {
|
|
id String @id @default(cuid())
|
|
phone String @unique
|
|
displayName String?
|
|
isActive Boolean @default(true)
|
|
authState AuthState @default(PENDING)
|
|
authCode String?
|
|
lastSeenAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
channelMaps AccountChannelMap[]
|
|
ingestionRuns IngestionRun[]
|
|
fetchRequests ChannelFetchRequest[]
|
|
skippedPackages SkippedPackage[]
|
|
|
|
@@index([isActive])
|
|
@@map("telegram_accounts")
|
|
}
|
|
|
|
model TelegramChannel {
|
|
id String @id @default(cuid())
|
|
telegramId BigInt @unique
|
|
title String
|
|
type ChannelType
|
|
isForum Boolean @default(false)
|
|
isActive Boolean @default(false)
|
|
category String? @db.VarChar(64)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
accountMaps AccountChannelMap[]
|
|
packages Package[]
|
|
skippedPackages SkippedPackage[]
|
|
packageGroups PackageGroup[]
|
|
|
|
@@index([type, isActive])
|
|
@@index([category])
|
|
@@map("telegram_channels")
|
|
}
|
|
|
|
model AccountChannelMap {
|
|
id String @id @default(cuid())
|
|
accountId String
|
|
channelId String
|
|
role ChannelRole @default(READER)
|
|
lastProcessedMessageId BigInt?
|
|
createdAt DateTime @default(now())
|
|
|
|
account TelegramAccount @relation(fields: [accountId], references: [id], onDelete: Cascade)
|
|
channel TelegramChannel @relation(fields: [channelId], references: [id], onDelete: Cascade)
|
|
topicProgress TopicProgress[]
|
|
|
|
@@unique([accountId, channelId])
|
|
@@index([accountId])
|
|
@@index([channelId])
|
|
@@map("account_channel_map")
|
|
}
|
|
|
|
model Package {
|
|
id String @id @default(cuid())
|
|
contentHash String @unique
|
|
fileName String
|
|
fileSize BigInt
|
|
archiveType ArchiveType
|
|
creator String?
|
|
sourceChannelId String
|
|
sourceMessageId BigInt
|
|
sourceTopicId BigInt?
|
|
destChannelId String?
|
|
destMessageId BigInt?
|
|
destMessageIds BigInt[] @default([])
|
|
isMultipart Boolean @default(false)
|
|
partCount Int @default(1)
|
|
fileCount Int @default(0)
|
|
tags String[] @default([])
|
|
sourceCaption String? // Caption text from source Telegram message
|
|
replyToMessageId BigInt? // reply_to_message_id from source message (for reply chain grouping)
|
|
previewData Bytes? // JPEG thumbnail from nearby Telegram photo (stored as raw bytes)
|
|
previewMsgId BigInt? // Telegram message ID of the matched photo
|
|
packageGroupId String?
|
|
indexedAt DateTime @default(now())
|
|
createdAt DateTime @default(now())
|
|
|
|
sourceChannel TelegramChannel @relation(fields: [sourceChannelId], references: [id])
|
|
packageGroup PackageGroup? @relation(fields: [packageGroupId], references: [id], onDelete: SetNull)
|
|
files PackageFile[]
|
|
ingestionRun IngestionRun? @relation(fields: [ingestionRunId], references: [id])
|
|
ingestionRunId String?
|
|
sendRequests BotSendRequest[]
|
|
extractRequests ArchiveExtractRequest[]
|
|
kickstarterLinks KickstarterPackage[]
|
|
|
|
@@index([sourceChannelId])
|
|
@@index([destChannelId])
|
|
@@index([fileName])
|
|
@@index([indexedAt])
|
|
@@index([archiveType])
|
|
@@index([creator])
|
|
@@index([packageGroupId])
|
|
@@map("packages")
|
|
}
|
|
|
|
model PackageFile {
|
|
id String @id @default(cuid())
|
|
packageId String
|
|
path String
|
|
fileName String
|
|
extension String?
|
|
compressedSize BigInt @default(0)
|
|
uncompressedSize BigInt @default(0)
|
|
crc32 String?
|
|
|
|
package Package @relation(fields: [packageId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([packageId])
|
|
@@index([extension])
|
|
@@index([fileName])
|
|
@@map("package_files")
|
|
}
|
|
|
|
model PackageGroup {
|
|
id String @id @default(cuid())
|
|
name String
|
|
mediaAlbumId String?
|
|
sourceChannelId String
|
|
groupingSource GroupingSource @default(MANUAL)
|
|
previewData Bytes?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
packages Package[]
|
|
sourceChannel TelegramChannel @relation(fields: [sourceChannelId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([mediaAlbumId, sourceChannelId])
|
|
@@index([sourceChannelId])
|
|
@@map("package_groups")
|
|
}
|
|
|
|
model IngestionRun {
|
|
id String @id @default(cuid())
|
|
accountId String
|
|
status IngestionStatus @default(RUNNING)
|
|
startedAt DateTime @default(now())
|
|
finishedAt DateTime?
|
|
messagesScanned Int @default(0)
|
|
zipsFound Int @default(0)
|
|
zipsDuplicate Int @default(0)
|
|
zipsIngested Int @default(0)
|
|
errorMessage String?
|
|
|
|
// Live activity tracking — written by worker in real-time
|
|
currentActivity String? // Human-readable: "Downloading pack.zip (part 2/5)"
|
|
currentStep String? // Machine-readable step key
|
|
currentChannel String? // Channel title being processed
|
|
currentFile String? // File currently being processed
|
|
currentFileNum Int? // Which archive set (1-indexed)
|
|
totalFiles Int? // Total archive sets found
|
|
downloadedBytes BigInt? // Current download progress in bytes
|
|
totalBytes BigInt? // Total size of current download
|
|
downloadPercent Int? // 0-100
|
|
lastActivityAt DateTime? // When activity was last updated
|
|
|
|
account TelegramAccount @relation(fields: [accountId], references: [id])
|
|
packages Package[]
|
|
|
|
@@index([accountId])
|
|
@@index([status])
|
|
@@index([startedAt])
|
|
@@map("ingestion_runs")
|
|
}
|
|
|
|
model TopicProgress {
|
|
id String @id @default(cuid())
|
|
accountChannelMapId String
|
|
topicId BigInt
|
|
topicName String?
|
|
lastProcessedMessageId BigInt?
|
|
|
|
accountChannelMap AccountChannelMap @relation(fields: [accountChannelMapId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([accountChannelMapId, topicId])
|
|
@@index([accountChannelMapId])
|
|
@@map("topic_progress")
|
|
}
|
|
|
|
model GlobalSetting {
|
|
key String @id @db.VarChar(64)
|
|
value String @db.Text
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@map("global_settings")
|
|
}
|
|
|
|
model InviteCode {
|
|
id String @id @default(cuid())
|
|
code String @unique @db.VarChar(32)
|
|
maxUses Int @default(1)
|
|
uses Int @default(0)
|
|
expiresAt DateTime?
|
|
createdBy String
|
|
createdAt DateTime @default(now())
|
|
|
|
creator User @relation("InviteCreator", fields: [createdBy], references: [id], onDelete: Cascade)
|
|
usedBy User[] @relation("InviteUser")
|
|
|
|
@@index([code])
|
|
@@map("invite_codes")
|
|
}
|
|
|
|
model ChannelFetchRequest {
|
|
id String @id @default(cuid())
|
|
accountId String
|
|
status FetchStatus @default(PENDING)
|
|
resultJson String? @db.Text
|
|
error String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
account TelegramAccount @relation(fields: [accountId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([accountId, status])
|
|
@@map("channel_fetch_requests")
|
|
}
|
|
|
|
// ───────────────────────────────────────
|
|
// Telegram Bot models
|
|
// ───────────────────────────────────────
|
|
|
|
enum BotSendStatus {
|
|
PENDING
|
|
SENDING
|
|
SENT
|
|
FAILED
|
|
}
|
|
|
|
/// Links a NextAuth user to a Telegram user ID.
|
|
/// Created when a user sends /link <code> to the bot.
|
|
model TelegramLink {
|
|
id String @id @default(cuid())
|
|
userId String @unique
|
|
telegramUserId BigInt @unique
|
|
telegramName String? @db.VarChar(128)
|
|
createdAt DateTime @default(now())
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
sendRequests BotSendRequest[]
|
|
|
|
@@map("telegram_links")
|
|
}
|
|
|
|
/// A queued request to send a package to a Telegram user via the bot.
|
|
model BotSendRequest {
|
|
id String @id @default(cuid())
|
|
packageId String
|
|
telegramLinkId String
|
|
requestedByUserId String
|
|
status BotSendStatus @default(PENDING)
|
|
error String?
|
|
createdAt DateTime @default(now())
|
|
completedAt DateTime?
|
|
|
|
package Package @relation(fields: [packageId], references: [id])
|
|
telegramLink TelegramLink @relation(fields: [telegramLinkId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([status])
|
|
@@index([telegramLinkId])
|
|
@@index([createdAt])
|
|
@@map("bot_send_requests")
|
|
}
|
|
|
|
/// Subscriptions for new-package notifications via the bot.
|
|
model BotSubscription {
|
|
id String @id @default(cuid())
|
|
telegramUserId BigInt
|
|
pattern String @db.VarChar(256)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([telegramUserId, pattern])
|
|
@@index([telegramUserId])
|
|
@@map("bot_subscriptions")
|
|
}
|
|
|
|
// ───────────────────────────────────────
|
|
// Archive image extraction (worker-mediated)
|
|
// ───────────────────────────────────────
|
|
|
|
enum ExtractStatus {
|
|
PENDING
|
|
IN_PROGRESS
|
|
COMPLETED
|
|
FAILED
|
|
}
|
|
|
|
/// A request for the worker to extract an image from an archive.
|
|
/// The web app creates this, sends a pg_notify, and the worker
|
|
/// downloads the archive, extracts the file, and writes the result.
|
|
model ArchiveExtractRequest {
|
|
id String @id @default(cuid())
|
|
packageId String
|
|
filePath String @db.VarChar(1024) // path within archive to extract
|
|
status ExtractStatus @default(PENDING)
|
|
imageData Bytes? // extracted image bytes (JPEG/PNG/WebP)
|
|
contentType String? @db.VarChar(64) // MIME type of extracted image
|
|
error String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
package Package @relation(fields: [packageId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([packageId, filePath])
|
|
@@index([status])
|
|
@@map("archive_extract_requests")
|
|
}
|
|
|
|
// ───────────────────────────────────────
|
|
// Skipped/Failed Archives
|
|
// ───────────────────────────────────────
|
|
|
|
enum SkipReason {
|
|
SIZE_LIMIT
|
|
DOWNLOAD_FAILED
|
|
EXTRACT_FAILED
|
|
UPLOAD_FAILED
|
|
}
|
|
|
|
model SkippedPackage {
|
|
id String @id @default(cuid())
|
|
fileName String
|
|
fileSize BigInt
|
|
reason SkipReason
|
|
errorMessage String?
|
|
sourceChannelId String
|
|
sourceChannel TelegramChannel @relation(fields: [sourceChannelId], references: [id], onDelete: Cascade)
|
|
sourceMessageId BigInt
|
|
sourceTopicId BigInt?
|
|
isMultipart Boolean @default(false)
|
|
partCount Int @default(1)
|
|
accountId String
|
|
account TelegramAccount @relation(fields: [accountId], references: [id], onDelete: Cascade)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([sourceChannelId, sourceMessageId])
|
|
@@index([reason])
|
|
@@index([accountId])
|
|
@@map("skipped_packages")
|
|
}
|
|
|
|
// ───────────────────────────────────────
|
|
// Purchased Kickstarters
|
|
// ───────────────────────────────────────
|
|
|
|
enum DeliveryStatus {
|
|
NOT_DELIVERED
|
|
PARTIAL
|
|
DELIVERED
|
|
}
|
|
|
|
enum PaymentStatus {
|
|
PAID
|
|
UNPAID
|
|
}
|
|
|
|
model KickstarterHost {
|
|
id String @id @default(cuid())
|
|
name String @unique
|
|
createdAt DateTime @default(now())
|
|
|
|
kickstarters Kickstarter[]
|
|
|
|
@@map("kickstarter_hosts")
|
|
}
|
|
|
|
model Kickstarter {
|
|
id String @id @default(cuid())
|
|
name String
|
|
link String?
|
|
filesUrl String?
|
|
deliveryStatus DeliveryStatus @default(NOT_DELIVERED)
|
|
paymentStatus PaymentStatus @default(UNPAID)
|
|
notes String?
|
|
hostId String?
|
|
userId String
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
host KickstarterHost? @relation(fields: [hostId], references: [id], onDelete: SetNull)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
packages KickstarterPackage[]
|
|
|
|
@@index([hostId])
|
|
@@index([userId])
|
|
@@index([deliveryStatus])
|
|
@@index([paymentStatus])
|
|
@@map("kickstarters")
|
|
}
|
|
|
|
model KickstarterPackage {
|
|
kickstarterId String
|
|
packageId String
|
|
|
|
kickstarter Kickstarter @relation(fields: [kickstarterId], references: [id], onDelete: Cascade)
|
|
package Package @relation(fields: [packageId], references: [id], onDelete: Cascade)
|
|
|
|
@@id([kickstarterId, packageId])
|
|
@@map("kickstarter_packages")
|
|
}
|
|
|
|
// ── Grouping & Notifications ──
|
|
|
|
enum GroupingSource {
|
|
ALBUM
|
|
MANUAL
|
|
AUTO_TIME
|
|
AUTO_PATTERN
|
|
AUTO_REPLY
|
|
AUTO_ZIP
|
|
AUTO_CAPTION
|
|
}
|
|
|
|
enum NotificationType {
|
|
HASH_MISMATCH
|
|
MISSING_PART
|
|
UPLOAD_FAILED
|
|
DOWNLOAD_FAILED
|
|
GROUPING_CONFLICT
|
|
INTEGRITY_AUDIT
|
|
}
|
|
|
|
enum NotificationSeverity {
|
|
INFO
|
|
WARNING
|
|
ERROR
|
|
}
|
|
|
|
model SystemNotification {
|
|
id String @id @default(cuid())
|
|
type NotificationType
|
|
severity NotificationSeverity @default(INFO)
|
|
title String
|
|
message String
|
|
context Json?
|
|
isRead Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([isRead, createdAt])
|
|
@@index([type])
|
|
@@map("system_notifications")
|
|
}
|