feat(cms): add recovery history and operational reliability tools
CI / check (push) Failing after 22s
CI / deploy (push) Skipped
CI / publish-container (push) Skipped

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

+47 -1
View File
@@ -1,7 +1,8 @@
import { randomUUID } from "node:crypto";
import { createHash, randomUUID } from "node:crypto";
import { promises as fs } from "node:fs";
import path from "node:path";
import type { ImportJob } from "@/lib/furni/import-job";
import { retryableJobItems } from "@/lib/furni/import-job-retry";
export const importRoot = () =>
path.join(process.cwd(), "storage", "furniture-imports");
export const validJobId = (value: unknown): value is string =>
@@ -73,6 +74,51 @@ export class ImportJobStore {
});
return this.read(id);
}
async retry(id: string, userId: number): Promise<ImportJob | null> {
const original = await this.read(id).catch((error) => {
if (error.code === "ENOENT") return null;
throw error;
});
if (!original || original.userId !== userId) return null;
const items = retryableJobItems(original);
if (!items.length) return null;
// A parent produces one durable child, including concurrent and uncertain responses.
const hex = createHash("sha256").update(`retry:${id}`).digest("hex");
const retryId = `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
const now = new Date().toISOString();
return this.create({
...original,
id: retryId,
retryOf: id,
createdAt: now,
updatedAt: now,
state: "queued",
cancelRequested: false,
items: items.map(
({ error: _error, warnings: _warnings, itemId: _itemId, ...item }) => ({
...item,
state: "pending",
}),
),
});
}
async page(options: { userId: number; limit: number; before?: string }) {
const limit = Math.min(30, Math.max(1, options.limit));
const key = (job: ImportJob) => `${job.createdAt}|${job.id}`;
const jobs = (await this.list())
.filter(
(job) =>
job.userId === options.userId &&
(!options.before || key(job) < options.before),
)
.sort((a, b) => key(b).localeCompare(key(a)));
const page = jobs.slice(0, limit);
return {
jobs: page,
nextCursor: jobs.length > limit ? key(page[page.length - 1]) : null,
};
}
async list(options?: {
userId: number;
limit: number;