feat(cms): add recovery history and operational reliability tools

This commit is contained in:
Simo committed 2026-09-09 21:57:56 +02:00
1 parent 89526af344
commit 1ef405be0a
92 files changed
+6384 -267

No files matched your search

+88 -1
View File
@@ -1,5 +1,6 @@
import { createHash } from "node:crypto";
import { sql } from "drizzle-orm";
import { db } from "@/lib/db";
import { AdminAuditLog, db } from "@/lib/db";
import {
readFurniData,
withFurniDataLock,
@@ -784,3 +785,89 @@ export async function repairFurniData(
return { added: added.length, removedDuplicates, removedIdConflicts };
});
}
export interface CatalogParentRepairPreview {
fingerprint: string;
catalog: "normal" | "bc";
changes: import("./catalog-integrity").ParentRepair[];
}
function parentStateFingerprint(
catalog: "normal" | "bc",
pages: import("./catalog-integrity").IntegrityPage[],
) {
return createHash("sha256")
.update(JSON.stringify({ catalog, pages }))
.digest("hex");
}
export async function previewCatalogParentRepair(
catalog: "normal" | "bc",
): Promise<CatalogParentRepairPreview> {
const { integrityCatalog, planParentRepairs } = await import(
"./catalog-integrity"
);
integrityCatalog(catalog);
const table = sql.raw(
catalog === "bc" ? "catalog_pages_bc" : "catalog_pages",
);
const [rows] = await db.execute(
sql`SELECT id, parent_id AS parentId, caption FROM ${table} ORDER BY id`,
);
const pages =
rows as unknown as import("./catalog-integrity").IntegrityPage[];
return {
catalog,
fingerprint: parentStateFingerprint(catalog, pages),
changes: planParentRepairs(pages),
};
}
/** Locks and revalidates the complete parent graph before making the previewed changes. */
export async function applyCatalogParentRepair(
catalog: "normal" | "bc",
fingerprint: string,
userId: number,
): Promise<{ applied: number }> {
const { integrityCatalog, planParentRepairs } = await import(
"./catalog-integrity"
);
integrityCatalog(catalog);
if (!/^[a-f0-9]{64}$/.test(fingerprint))
throw new Error("Invalid repair preview");
const tableName = catalog === "bc" ? "catalog_pages_bc" : "catalog_pages";
const table = sql.raw(tableName);
return db.transaction(async (tx) => {
const [rows] = await tx.execute(
sql`SELECT id, parent_id AS parentId, caption FROM ${table} ORDER BY id FOR UPDATE`,
);
const pages =
rows as unknown as import("./catalog-integrity").IntegrityPage[];
if (parentStateFingerprint(catalog, pages) !== fingerprint) {
const error = new Error("Catalog changed since preview");
error.name = "CatalogPreviewConflict";
throw error;
}
const changes = planParentRepairs(pages);
for (const change of changes) {
await tx.execute(
sql`UPDATE ${table} SET parent_id = ${change.to} WHERE id = ${change.id} AND parent_id = ${change.from}`,
);
await tx
.insert(AdminAuditLog)
.values({
userId,
action: "catalog_parent_repair",
target: tableName,
targetId: change.id,
before: JSON.stringify({ parentId: change.from }),
after: JSON.stringify({ parentId: change.to }),
diff: JSON.stringify({
parentId: { from: change.from, to: change.to },
}),
createdAt: new Date().toISOString(),
});
}
return { applied: changes.length };
});
}