mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-05-11 06:11:15 +00:00
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) <noreply@anthropic.com>
This commit is contained in:
117
worker/src/audit.ts
Normal file
117
worker/src/audit.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { db } from "./db/client.js";
|
||||
import { childLogger } from "./util/logger.js";
|
||||
|
||||
const log = childLogger("audit");
|
||||
|
||||
/**
|
||||
* Periodic integrity audit: checks all packages for consistency.
|
||||
* Creates SystemNotification records for any issues found.
|
||||
*
|
||||
* Checks performed:
|
||||
* 1. Multipart completeness: destMessageIds.length should match partCount
|
||||
* 2. Missing destination: packages with destChannelId but no destMessageId
|
||||
*/
|
||||
export async function runIntegrityAudit(): Promise<{ checked: number; issues: number }> {
|
||||
log.info("Starting integrity audit");
|
||||
|
||||
let checked = 0;
|
||||
let issues = 0;
|
||||
|
||||
// Check 1: Multipart packages with wrong number of destination message IDs
|
||||
const multipartPackages = await db.package.findMany({
|
||||
where: {
|
||||
isMultipart: true,
|
||||
partCount: { gt: 1 },
|
||||
destMessageId: { not: null },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
fileName: true,
|
||||
partCount: true,
|
||||
destMessageIds: true,
|
||||
sourceChannelId: true,
|
||||
sourceChannel: { select: { title: true } },
|
||||
},
|
||||
});
|
||||
|
||||
checked += multipartPackages.length;
|
||||
|
||||
for (const pkg of multipartPackages) {
|
||||
const actualParts = pkg.destMessageIds.length;
|
||||
if (actualParts > 0 && actualParts !== pkg.partCount) {
|
||||
issues++;
|
||||
|
||||
// Check if we already have a notification for this
|
||||
const existing = await db.systemNotification.findFirst({
|
||||
where: {
|
||||
type: "MISSING_PART",
|
||||
context: { path: ["packageId"], equals: pkg.id },
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
await db.systemNotification.create({
|
||||
data: {
|
||||
type: "MISSING_PART",
|
||||
severity: "WARNING",
|
||||
title: `Incomplete multipart: ${pkg.fileName}`,
|
||||
message: `Expected ${pkg.partCount} parts but only ${actualParts} destination message IDs stored`,
|
||||
context: {
|
||||
packageId: pkg.id,
|
||||
fileName: pkg.fileName,
|
||||
expectedParts: pkg.partCount,
|
||||
actualParts,
|
||||
sourceChannelId: pkg.sourceChannelId,
|
||||
channelTitle: pkg.sourceChannel.title,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
log.warn(
|
||||
{ packageId: pkg.id, fileName: pkg.fileName, expected: pkg.partCount, actual: actualParts },
|
||||
"Multipart package has mismatched part count"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check 2: Packages with dest channel but no dest message (orphaned index)
|
||||
const orphanedCount = await db.package.count({
|
||||
where: {
|
||||
destChannelId: { not: null },
|
||||
destMessageId: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (orphanedCount > 0) {
|
||||
issues++;
|
||||
|
||||
const existing = await db.systemNotification.findFirst({
|
||||
where: {
|
||||
type: "INTEGRITY_AUDIT",
|
||||
context: { path: ["check"], equals: "orphaned_index" },
|
||||
createdAt: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) },
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
await db.systemNotification.create({
|
||||
data: {
|
||||
type: "INTEGRITY_AUDIT",
|
||||
severity: "INFO",
|
||||
title: `${orphanedCount} packages with missing destination message`,
|
||||
message: `Found ${orphanedCount} packages that have a destination channel set but no destination message ID. These may be from interrupted uploads.`,
|
||||
context: {
|
||||
check: "orphaned_index",
|
||||
count: orphanedCount,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
log.info({ checked, issues }, "Integrity audit complete");
|
||||
return { checked, issues };
|
||||
}
|
||||
Reference in New Issue
Block a user