diff --git a/backend/src/routes/admin.ts b/backend/src/routes/admin.ts index 30534d4..944616c 100644 --- a/backend/src/routes/admin.ts +++ b/backend/src/routes/admin.ts @@ -401,6 +401,12 @@ export default async function adminRoutes(server: FastifyInstance) { contentType: string, filename: string ) => { + const heartbeat = setInterval(() => { + void server.db + .update(tenantExportJobs) + .set({ updatedAt: new Date() }) + .where(eq(tenantExportJobs.id, jobId)); + }, 15_000); try { const exportData = await parseMergeSource(source, contentType, filename); const report = sanitizeTenantMergePlan(await createTenantMergeDryRun(exportData, targetTenantId)); @@ -434,6 +440,8 @@ export default async function adminRoutes(server: FastifyInstance) { updatedAt: new Date(), }) .where(eq(tenantExportJobs.id, jobId)); + } finally { + clearInterval(heartbeat); } }; @@ -1466,11 +1474,25 @@ export default async function adminRoutes(server: FastifyInstance) { if (!job) return reply.code(404).send({ error: "Export not found" }); + let returnedStatus = job.status; + const staleMergeReview = job.operation === "merge" + && ["running", "recovering"].includes(job.status) + && (!job.updatedAt || Date.now() - new Date(job.updatedAt).getTime() > 60_000); + if (staleMergeReview && job.storagePath) { + const source = await readS3Buffer(job.storagePath); + await server.db + .update(tenantExportJobs) + .set({ status: "recovering", updatedAt: new Date(), error: null }) + .where(eq(tenantExportJobs.id, job.id)); + void startTenantMergeReviewJob(job.id, job.tenantId, source, job.contentType, job.filename); + returnedStatus = "recovering"; + } + return { exportId: job.id, tenantId: job.tenantId, operation: job.operation, - status: job.status, + status: returnedStatus, filename: job.filename, fileSize: job.fileSize, filesDone: job.filesDone, diff --git a/backend/src/utils/tenantMergePlan.ts b/backend/src/utils/tenantMergePlan.ts index a4391a3..bcc0dd4 100644 --- a/backend/src/utils/tenantMergePlan.ts +++ b/backend/src/utils/tenantMergePlan.ts @@ -53,9 +53,13 @@ const comparableRow = (row: Record) => Object.fromEntries( .sort(([left], [right]) => left.localeCompare(right)) ) -const rowDifferences = (source: Record, target: Record) => { +const rowDifferences = (table: string, source: Record, target: Record) => { const sourceComparable = comparableRow(source) const targetComparable = comparableRow(target) + if (table === "citys") { + delete sourceComparable.geometry + delete targetComparable.geometry + } const columns = Array.from(new Set([ ...Object.keys(sourceComparable), ...Object.keys(targetComparable), @@ -111,7 +115,7 @@ export const buildTenantMergePlan = ( const naturalTarget = sourceNaturalKey ? targetByNaturalKey.get(sourceNaturalKey) : undefined const primaryTarget = targetByPrimaryKey.get(sourceKey) const targetRow = naturalTarget || primaryTarget || null - const differences = targetRow ? rowDifferences(sourceRow, targetRow) : [] + const differences = targetRow ? rowDifferences(table, sourceRow, targetRow) : [] let kind: TenantMergeKind if (!targetRow) { diff --git a/backend/src/utils/tenantMergeService.ts b/backend/src/utils/tenantMergeService.ts index f686ee9..695fc91 100644 --- a/backend/src/utils/tenantMergeService.ts +++ b/backend/src/utils/tenantMergeService.ts @@ -161,16 +161,43 @@ const sensitiveColumns = new Set([ "tokenHash", ]) -const redactRow = (row: Record | null) => row && Object.fromEntries( - Object.entries(row).map(([column, value]) => [column, sensitiveColumns.has(column) && value ? "***" : value]) -) +const compactValue = (value: any) => { + if (value === null || typeof value !== "object") return value + const serialized = JSON.stringify(value) + return `[Struktur mit ${serialized.length} Zeichen]` +} + +const compactReviewRow = (row: Record | null, differences: string[]) => { + if (!row) return null + const displayColumns = new Set([ + "id", + "tenant", + "tenant_id", + "name", + "label", + "title", + "number", + "email", + "zip", + "short", + "accountChart", + ...differences, + ]) + + return Object.fromEntries(Object.entries(row) + .filter(([column]) => displayColumns.has(column)) + .map(([column, value]) => [ + column, + sensitiveColumns.has(column) && value ? "***" : compactValue(value), + ])) +} export const sanitizeTenantMergePlan = (plan: TenantMergePlan): TenantMergePlan => ({ ...plan, - items: plan.items.map((item) => ({ + items: plan.items.filter((item) => item.kind !== "existing").map((item) => ({ ...item, - sourceRow: redactRow(item.sourceRow) || {}, - targetRow: redactRow(item.targetRow), + sourceRow: compactReviewRow(item.sourceRow, item.differences) || {}, + targetRow: compactReviewRow(item.targetRow, item.differences), })), })