feat(cms): improve catalog, editorial recovery and operations
CI / check (push) Successful in 52s
CI / deploy (push) Successful in 2m10s
CI / publish-container (push) Failing after 1m18s

This commit is contained in:
Simo committed 2026-09-09 19:36:15 +02:00
1 parent 2fead134e6
commit c389c3893d
122 files changed
+8137 -703

No files matched your search

+31 -3
View File
@@ -5,7 +5,14 @@ import { retryableJobItems } from "./import-job-retry";
const job = (state: ImportJob["state"]) =>
({
state,
items: ["done", "failed", "pending", "running"].map((state) => ({
items: [
"done",
"failed",
"pending",
"running",
"cancelled",
"interrupted",
].map((state) => ({
state,
classname: state,
})),
@@ -14,12 +21,33 @@ it("only retries failures after a completed job", () =>
expect(retryableJobItems(job("completed")).map((i) => i.state)).toEqual([
"failed",
]));
it("recovers all unfinished items from an interrupted job", () =>
it("recovers only safe unfinished items from an interrupted job", () =>
expect(retryableJobItems(job("interrupted")).map((i) => i.state)).toEqual([
"failed",
"pending",
"running",
"cancelled",
]));
it.each(["queued", "running"] as const)("never duplicates a %s job", (state) =>
expect(retryableJobItems(job(state))).toEqual([]),
);
it("retries cancelled remaining work without redoing completed or uncertain items", () => {
expect(retryableJobItems(job("cancelled")).map((i) => i.state)).toEqual([
"failed",
"pending",
"cancelled",
]);
});
it("does not blindly retry legacy interrupted mutations stored as failures", () => {
const interrupted = job("interrupted");
interrupted.items = [
{
...interrupted.items[0],
state: "failed",
error:
"Server restarted during import. Review local data and retry to complete missing parts.",
},
];
expect(retryableJobItems(interrupted)).toEqual([]);
});
+4 -2
View File
@@ -4,7 +4,9 @@ export function retryableJobItems(job: ImportJob): ImportJob["items"] {
if (job.state === "running" || job.state === "queued") return [];
return job.items.filter(
(item) =>
item.state === "failed" ||
(job.state === "interrupted" && item.state !== "done"),
(item.state === "failed" &&
!item.error?.startsWith("Server restarted during import.")) ||
((job.state === "interrupted" || job.state === "cancelled") &&
(item.state === "pending" || item.state === "cancelled")),
);
}
+9 -2
View File
@@ -14,13 +14,20 @@ export interface ImportJob {
userId: number;
createdAt: string;
updatedAt: string;
state: "queued" | "running" | "completed" | "interrupted";
state: "queued" | "running" | "completed" | "interrupted" | "cancelled";
cancelRequested?: boolean;
sourceId?: string;
translate: boolean;
langs?: string[];
items: Array<
ImportJobItem & {
state: "pending" | "running" | "done" | "failed";
state:
| "pending"
| "running"
| "done"
| "failed"
| "cancelled"
| "interrupted";
error?: string;
warnings?: string[];
itemId?: number;
+93
View File
@@ -0,0 +1,93 @@
import { describe, expect, it } from "vitest";
import { furnitureCompleteness } from "./studio-completeness";
import {
compareFurniture,
type FurnitureInspection,
} from "./studio-inspection";
const complete: FurnitureInspection = {
classname: "chair",
sql: [{ id: 100, spriteId: 200, type: "s", name: "Chair" }],
catalog: [{ id: 50, pageId: 1, credits: 3, points: 0 }],
furnidata: [
{ id: 200, type: "flooritem", name: "Chair", description: "", revision: 1 },
],
furnidataReadable: true,
nitro: { exists: true, bytes: 200 },
icon: { exists: true, bytes: 30 },
};
describe("furniture completeness", () => {
it("checks all five independent parts and accepts locally remapped IDs", () => {
expect(furnitureCompleteness(complete).map((p) => p.status)).toEqual(
Array(5).fill("present"),
);
});
it("does not infer a catalog offer or icon from SQL and Nitro", () => {
const parts = furnitureCompleteness({
...complete,
catalog: [],
icon: { exists: false, bytes: 0 },
});
expect(parts.find((p) => p.id === "catalog")).toMatchObject({
status: "missing",
action: "catalog",
});
expect(parts.find((p) => p.id === "icon")).toMatchObject({
status: "missing",
action: "icon",
});
expect(parts.find((p) => p.id === "sql")?.status).toBe("present");
});
it("keeps unreadable assets unknown and directs operators to recheck", () => {
const parts = furnitureCompleteness({
...complete,
furnidataReadable: false,
icon: { exists: null, bytes: 0 },
});
expect(parts.find((p) => p.id === "furnidata")).toMatchObject({
status: "unknown",
action: "recheck",
});
expect(parts.find((p) => p.id === "icon")).toMatchObject({
status: "unknown",
action: "recheck",
});
});
it.each([
{ id: 201, type: "flooritem" },
{ id: 200, type: "wallitem" },
])(
"flags ID or type mismatches in both review and completeness: %j",
(change) => {
const local = {
...complete,
furnidata: [{ ...complete.furnidata[0], ...change }],
};
expect(
furnitureCompleteness(local)
.filter((p) => p.status === "conflict")
.map((p) => p.id),
).toEqual(["sql", "furnidata"]);
expect(
compareFurniture(
{ name: "Chair", description: "", revision: 1, type: "flooritem" },
local,
).state,
).toBe("conflict");
},
);
it("allows multiple legitimate catalog offers but surfaces duplicate SQL records", () => {
expect(
furnitureCompleteness({
...complete,
catalog: [...complete.catalog, { ...complete.catalog[0], id: 51 }],
}).find((p) => p.id === "catalog")?.status,
).toBe("present");
expect(
furnitureCompleteness({
...complete,
sql: [...complete.sql, { ...complete.sql[0], id: 101 }],
}).find((p) => p.id === "sql"),
).toMatchObject({ status: "conflict", action: "audit" });
});
});
+38
View File
@@ -0,0 +1,38 @@
import {
type FurnitureInspection,
hasFurnitureConflict,
} from "./studio-inspection";
export type FurniturePartId =
| "sql"
| "catalog"
| "furnidata"
| "icon"
| "nitro";
export interface FurniturePart {
id: FurniturePartId;
status: "present" | "missing" | "unknown" | "conflict";
action: FurniturePartId | "recheck" | "audit" | null;
}
/** Report each required part without treating unavailable checks as missing data. */
export function furnitureCompleteness(
local: FurnitureInspection,
): FurniturePart[] {
const conflict = hasFurnitureConflict(local);
const evidence: Array<[FurniturePartId, boolean | null]> = [
["sql", local.sql.length > 0],
["catalog", local.catalog.length > 0],
["furnidata", local.furnidataReadable ? local.furnidata.length > 0 : null],
["icon", local.icon.exists],
["nitro", local.nitro.exists],
];
return evidence.map(([id, present]) => {
if (conflict && (id === "sql" || id === "furnidata"))
return { id, status: "conflict", action: "audit" };
if (present === null) return { id, status: "unknown", action: "recheck" };
return {
id,
status: present ? "present" : "missing",
action: present ? null : id,
};
});
}
+25 -21
View File
@@ -56,16 +56,15 @@ export function compareFurniture(
after !== undefined &&
String(before) !== String(after),
}));
const state =
local.sql.length > 1 || local.furnidata.length > 1
? "conflict"
: !local.furnidataReadable
? "unknown"
: !sql && !fd
? "new"
: fields.some((f) => f.changed)
? "changed"
: "present";
const state = hasFurnitureConflict(local)
? "conflict"
: !local.furnidataReadable
? "unknown"
: !sql && !fd
? "new"
: fields.some((f) => f.changed)
? "changed"
: "present";
return { state, fields } as const;
}
export function furnitureHealth(local: FurnitureInspection) {
@@ -76,17 +75,7 @@ export function furnitureHealth(local: FurnitureInspection) {
missing.push("Furnidata");
if (!local.sql.length) missing.push("SQL item");
if (!local.catalog.length) missing.push("Catalog offer");
const sql = local.sql[0],
fd = local.furnidata[0];
const conflict =
local.sql.length > 1 ||
local.furnidata.length > 1 ||
!!(
sql &&
fd &&
(sql.spriteId !== fd.id ||
(sql.type === "i" ? "wallitem" : "flooritem") !== fd.type)
);
const conflict = hasFurnitureConflict(local);
const unknown =
!local.furnidataReadable ||
local.nitro.exists === null ||
@@ -102,3 +91,18 @@ export function furnitureHealth(local: FurnitureInspection) {
: "incomplete";
return { state, missing } as const;
}
export function hasFurnitureConflict(local: FurnitureInspection): boolean {
const sql = local.sql[0],
fd = local.furnidata[0];
return (
local.sql.length > 1 ||
local.furnidata.length > 1 ||
!!(
sql &&
fd &&
(sql.spriteId !== fd.id ||
(sql.type === "i" ? "wallitem" : "flooritem") !== fd.type)
)
);
}