feat(stls): flat package list, drop heuristic auto-grouping

Render the STL view as a flat per-package list (listDisplayItems no longer
collapses packages into group rows) and hide the Ungrouped tab, now that the
creator filter organizes the list. Remove the worker's heuristic auto-grouping
passes (rule/time/pattern/creator/zip-path/reply-chain/caption); album grouping
is kept. Existing groups and manual grouping actions are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-23 11:05:18 +02:00
co-authored by Claude Opus 4.8
parent 2252ac01f5
commit 21bd46010f
3 changed files with 23 additions and 49 deletions
+3 -8
View File
@@ -500,14 +500,9 @@ export function StlTable({
</Badge>
)}
</TabsTrigger>
<TabsTrigger value="ungrouped" className="gap-1.5">
Ungrouped
{ungroupedTotalCount > 0 && (
<Badge variant="secondary" className="h-5 px-1.5 text-[10px]">
{ungroupedTotalCount}
</Badge>
)}
</TabsTrigger>
{/* "Ungrouped" tab hidden: the STL list is now flat and grouping is
no longer surfaced here. The tab content below is kept (unreachable)
to avoid churn; remove it and its data fetch if grouping is dropped. */}
</TabsList>
<TabsContent value="packages" className="space-y-4">
+12 -12
View File
@@ -135,31 +135,31 @@ export async function listDisplayItems(options: {
const sortCol = sortBy === "fileName" ? `"fileName"` : sortBy === "fileSize" ? `"fileSize"` : `"indexedAt"`;
const sortDir = order === "asc" ? "ASC" : "DESC";
// Step 1: Count display items
// NOTE: The STL list is intentionally FLAT — every package is its own display
// row regardless of packageGroupId. Grouping is no longer surfaced in this
// view (the creator column/filter organizes the list instead). PackageGroup
// rows and the manual grouping actions still exist in the DB/UI; they just
// don't drive this list's layout anymore.
// Step 1: Count display items (one per package)
const countResult = await prisma.$queryRawUnsafe<[{ count: bigint }]>(
`SELECT COUNT(*) AS count FROM (
SELECT DISTINCT COALESCE(p."packageGroupId", p."id") AS display_id
FROM packages p
${whereClause}
) AS display_items`,
`SELECT COUNT(*) AS count FROM packages p ${whereClause}`,
...params
);
const total = Number(countResult[0].count);
// Step 2: Get display item IDs for this page
// Step 2: Get package IDs for this page
const limitParam = paramIdx++;
const offsetParam = paramIdx++;
const displayRows = await prisma.$queryRawUnsafe<
{ display_id: string; display_type: string }[]
>(
`SELECT
COALESCE(p."packageGroupId", p."id") AS display_id,
CASE WHEN p."packageGroupId" IS NOT NULL THEN 'group' ELSE 'package' END AS display_type,
MAX(p.${sortCol}) AS sort_value
p."id" AS display_id,
'package' AS display_type,
p.${sortCol} AS sort_value
FROM packages p
${whereClause}
GROUP BY COALESCE(p."packageGroupId", p."id"),
CASE WHEN p."packageGroupId" IS NOT NULL THEN 'group' ELSE 'package' END
ORDER BY sort_value ${sortDir}
LIMIT $${limitParam} OFFSET $${offsetParam}`,
...params, limit, (page - 1) * limit
+8 -29
View File
@@ -65,7 +65,7 @@ import { readRarContents } from "./archive/rar-reader.js";
import { read7zContents } from "./archive/sevenz-reader.js";
import { byteLevelSplit, concatenateFiles } from "./archive/split.js";
import { uploadToChannel, UploadStallError } from "./upload/channel.js";
import { processAlbumGroups, processRuleBasedGroups, processTimeWindowGroups, processPatternGroups, processCreatorGroups, processZipPathGroups, processReplyChainGroups, processCaptionGroups, detectGroupingConflicts, type IndexedPackageRef } from "./grouping.js";
import { processAlbumGroups, detectGroupingConflicts, type IndexedPackageRef } from "./grouping.js";
import { db } from "./db/client.js";
import type { TelegramAccount, TelegramChannel } from "@prisma/client";
import type { Client } from "tdl";
@@ -1479,34 +1479,13 @@ async function processArchiveSets(
scanResult.photos
);
// Auto-grouping passes (gated by per-channel flag)
const channelRecord = await db.telegramChannel.findUnique({
where: { id: channel.id },
select: { autoGroupEnabled: true },
});
if (channelRecord?.autoGroupEnabled !== false) {
// Learned rule-based grouping (from manual overrides)
await processRuleBasedGroups(channel.id, indexedPackageRefs);
// Time-window grouping for remaining ungrouped packages
await processTimeWindowGroups(channel.id, indexedPackageRefs);
// Pattern-based grouping (date patterns, project slugs)
await processPatternGroups(channel.id, indexedPackageRefs);
// Creator-based grouping (3+ files from same creator)
await processCreatorGroups(channel.id, indexedPackageRefs);
// ZIP path prefix grouping (shared root folder inside archives)
await processZipPathGroups(channel.id, indexedPackageRefs);
// Reply chain grouping (messages replying to same root)
await processReplyChainGroups(channel.id, indexedPackageRefs);
// Caption fuzzy match grouping
await processCaptionGroups(channel.id, indexedPackageRefs);
}
// Heuristic auto-grouping passes (rule/time/pattern/creator/zip-path/
// reply-chain/caption) were removed: the STL view is now a flat list
// organized by the creator filter, so automatically inventing groups at
// ingestion is no longer wanted. Album grouping above is kept because it
// reflects real upload structure (files posted together as one Telegram
// album), not a heuristic guess. Existing groups and the manual grouping
// actions in the UI are unaffected.
// Check for potential grouping conflicts
await detectGroupingConflicts(channel.id, indexedPackageRefs);