feat: fix channel scanning bugs, add package tags, and kickstarters tab

Bug fixes:
- Fix channels not being scanned by paginating TDLib getChats (was only
  loading first batch, additional channels were unknown to TDLib)
- Add per-channel getChat pre-load as safety net before scanning
- Fix preview pictures not loading by checking previewData instead of
  previewMsgId for hasPreview flag
- Prevent previewMsgId from being set when preview download fails

Package Tags:
- Add tags Text[] column to Package with migration backfilling from
  channel categories
- Worker auto-inherits source channel category as initial tag
- Tag filter dropdown and Tags column in STL Files table
- Server actions for individual and bulk tag editing

Kickstarters Tab:
- New KickstarterHost, Kickstarter, and KickstarterPackage models
- Full CRUD with delivery status, payment status, host management
- Package linking (many-to-many with existing packages)
- Sidebar entry with Gift icon
- Table with search, filters, modal forms

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-23 18:17:44 +01:00
parent e2dd3bb9d0
commit 5fd341dfc4
23 changed files with 1375 additions and 32 deletions

View File

@@ -11,12 +11,14 @@ export async function listPackages(options: {
limit: number;
channelId?: string;
creator?: string;
tag?: string;
sortBy: "indexedAt" | "fileName" | "fileSize";
order: "asc" | "desc";
}) {
const where: Record<string, unknown> = {};
if (options.channelId) where.sourceChannelId = options.channelId;
if (options.creator) where.creator = options.creator;
if (options.tag) where.tags = { has: options.tag };
const [items, total] = await Promise.all([
prisma.package.findMany({
@@ -34,7 +36,8 @@ export async function listPackages(options: {
isMultipart: true,
indexedAt: true,
creator: true,
previewMsgId: true, // cheap null check — avoids loading blob
tags: true,
previewData: true, // check actual image data, not previewMsgId proxy
sourceChannel: { select: { id: true, title: true } },
},
}),
@@ -49,8 +52,9 @@ export async function listPackages(options: {
archiveType: pkg.archiveType,
fileCount: pkg.fileCount,
isMultipart: pkg.isMultipart,
hasPreview: pkg.previewMsgId !== null,
hasPreview: pkg.previewData !== null,
creator: pkg.creator,
tags: pkg.tags,
indexedAt: pkg.indexedAt.toISOString(),
sourceChannel: pkg.sourceChannel,
}));
@@ -96,8 +100,9 @@ export async function getPackageById(
archiveType: pkg.archiveType,
fileCount: pkg.fileCount,
isMultipart: pkg.isMultipart,
hasPreview: pkg.previewMsgId !== null,
hasPreview: pkg.previewData !== null,
creator: pkg.creator,
tags: pkg.tags,
partCount: pkg.partCount,
indexedAt: pkg.indexedAt.toISOString(),
sourceChannel: pkg.sourceChannel,
@@ -208,7 +213,8 @@ export async function searchPackages(options: {
isMultipart: true,
indexedAt: true,
creator: true,
previewMsgId: true,
tags: true,
previewData: true,
sourceChannel: { select: { id: true, title: true } },
},
}),
@@ -223,8 +229,9 @@ export async function searchPackages(options: {
archiveType: pkg.archiveType,
fileCount: pkg.fileCount,
isMultipart: pkg.isMultipart,
hasPreview: pkg.previewMsgId !== null,
hasPreview: pkg.previewData !== null,
creator: pkg.creator,
tags: pkg.tags,
indexedAt: pkg.indexedAt.toISOString(),
sourceChannel: pkg.sourceChannel,
}));
@@ -249,6 +256,16 @@ export async function searchPackages(options: {
});
}
/**
* Get all distinct tags across all packages (for filter dropdowns).
*/
export async function getAllPackageTags(): Promise<string[]> {
const result = await prisma.$queryRaw<{ tag: string }[]>`
SELECT DISTINCT unnest(tags) AS tag FROM packages ORDER BY tag
`;
return result.map((r) => r.tag);
}
export async function getIngestionStatus(): Promise<IngestionAccountStatus[]> {
const accounts = await prisma.telegramAccount.findMany({
orderBy: { createdAt: "asc" },