From 7f9a03d4ee72bee7b4a230cde2d203d745521a46 Mon Sep 17 00:00:00 2001 From: xCyanGrizzly Date: Mon, 30 Mar 2026 14:19:36 +0200 Subject: [PATCH] feat: group merge, ZIP/reply/caption grouping, integrity audit 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) --- .../migration.sql | 3 + prisma/schema.prisma | 2 + .../stls/_components/package-columns.tsx | 30 ++- src/app/(app)/stls/_components/stl-table.tsx | 36 +++ src/app/(app)/stls/actions.ts | 21 ++ src/lib/telegram/queries.ts | 10 + worker/src/archive/multipart.ts | 2 + worker/src/audit.ts | 117 +++++++++ worker/src/db/queries.ts | 6 +- worker/src/grouping.ts | 237 ++++++++++++++++++ worker/src/scheduler.ts | 11 + worker/src/tdlib/download.ts | 3 + worker/src/worker.ts | 13 +- 13 files changed, 488 insertions(+), 3 deletions(-) create mode 100644 prisma/migrations/20260330130000_add_caption_and_reply_to/migration.sql create mode 100644 worker/src/audit.ts diff --git a/prisma/migrations/20260330130000_add_caption_and_reply_to/migration.sql b/prisma/migrations/20260330130000_add_caption_and_reply_to/migration.sql new file mode 100644 index 0000000..63bcbd4 --- /dev/null +++ b/prisma/migrations/20260330130000_add_caption_and_reply_to/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable: add sourceCaption and replyToMessageId to packages +ALTER TABLE "packages" ADD COLUMN "sourceCaption" TEXT; +ALTER TABLE "packages" ADD COLUMN "replyToMessageId" BIGINT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ea0a150..aa3400a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -474,6 +474,8 @@ model Package { 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? diff --git a/src/app/(app)/stls/_components/package-columns.tsx b/src/app/(app)/stls/_components/package-columns.tsx index f51c45d..c0385fa 100644 --- a/src/app/(app)/stls/_components/package-columns.tsx +++ b/src/app/(app)/stls/_components/package-columns.tsx @@ -1,7 +1,7 @@ "use client"; import { type ColumnDef } from "@tanstack/react-table"; -import { FileArchive, Eye, ChevronRight, Layers, Ungroup, Send, ImagePlus } from "lucide-react"; +import { FileArchive, Eye, ChevronRight, Layers, Ungroup, Send, ImagePlus, GitMerge } from "lucide-react"; import { DataTableColumnHeader } from "@/components/shared/data-table-column-header"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -69,6 +69,9 @@ interface PackageColumnsProps { onGroupPreviewUpload: (groupId: string) => void; selectedPackages: Set; onToggleSelect: (packageId: string) => void; + mergeSourceId: string | null; + onStartMerge: (groupId: string) => void; + onCompleteMerge: (targetGroupId: string) => void; } export function formatBytes(bytesStr: string): string { @@ -148,6 +151,9 @@ export function getPackageColumns({ onGroupPreviewUpload, selectedPackages, onToggleSelect, + mergeSourceId, + onStartMerge, + onCompleteMerge, }: PackageColumnsProps): ColumnDef[] { return [ { @@ -392,6 +398,8 @@ export function getPackageColumns({ cell: ({ row }) => { const data = row.original; if (isGroupRow(data)) { + const isMergeSource = mergeSourceId === data.id; + const canMergeHere = mergeSourceId !== null && mergeSourceId !== data.id; return (
+ + {canMergeHere && ( + + )}