Block invalid catalog_items writes at the API level (points currency allowlist, non-negative prices, positive amount, limited stack >= sold count, unique sibling order numbers) and auto-assign unique order numbers on bulk create. Add a catalog-maintenance scan + transactional repair that fixes pre-existing rows: resets unsupported points_type, clamps negative costs, sets amount to 1, raises limited_stack, renumbers duplicate orders and deletes offers with missing page/item references. Surface the issue count and a fix button in the admin maintenance panel. Also: add enabled/retired flag to clone sources, classify poster and currency furniture in item-kind, and remove the obsolete update-Nitrov3.sh.
83 lines
3.0 KiB
TypeScript
83 lines
3.0 KiB
TypeScript
// Runs repairMissingIcons + repairMissingNitros directly (no source comparison
|
|
// phase, which is the slow part of runCatalogAudit). Logs progress to a file.
|
|
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
import { beforeAll, describe, expect, it } from "vitest";
|
|
|
|
const runLive = process.env.RUN_CATALOG_AUDIT_LIVE === "1";
|
|
const LOG = "/tmp/catalog-asset-repair.log";
|
|
const log = (msg: string) => {
|
|
appendFileSync(LOG, `${new Date().toISOString()} ${msg}\n`);
|
|
};
|
|
|
|
describe.skipIf(!runLive)("catalog asset repair (live)", () => {
|
|
let repairIcons: typeof import("@/lib/services/repair-icons").repairMissingIcons;
|
|
let repairNitros: typeof import("@/lib/services/repair-nitros").repairMissingNitros;
|
|
|
|
beforeAll(async () => {
|
|
const envFile = resolve(process.cwd(), ".env");
|
|
if (existsSync(envFile)) {
|
|
for (const line of readFileSync(envFile, "utf8").split(/\r?\n/)) {
|
|
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
|
|
if (!m) continue;
|
|
let value = m[2].trim();
|
|
if (
|
|
(value.startsWith('"') && value.endsWith('"')) ||
|
|
(value.startsWith("'") && value.endsWith("'"))
|
|
) {
|
|
value = value.slice(1, -1);
|
|
}
|
|
process.env[m[1]] = value;
|
|
}
|
|
}
|
|
process.env["SKIP_ENV_VALIDATION"] = "1";
|
|
|
|
[repairIcons, repairNitros] = await Promise.all([
|
|
import("@/lib/services/repair-icons").then((m) => m.repairMissingIcons),
|
|
import("@/lib/services/repair-nitros").then((m) => m.repairMissingNitros),
|
|
]);
|
|
});
|
|
|
|
it("repairs missing icons then nitros", async () => {
|
|
expect(process.env.DATABASE_URL).toMatch(/@/);
|
|
|
|
log("=== ICON REPAIR START ===");
|
|
let lastPct = -1;
|
|
const iconResult = await repairIcons((evt) => {
|
|
if (evt.type === "progress" && evt.total) {
|
|
const pct = Math.round(((evt.index ?? 0) / evt.total) * 100);
|
|
if (pct % 5 === 0 && pct !== lastPct) {
|
|
lastPct = pct;
|
|
log(`icons ${evt.index}/${evt.total} (${pct}%)`);
|
|
}
|
|
if (evt.status === "done") log(`ICON DONE: ${evt.classname}`);
|
|
if (evt.status === "failed")
|
|
log(`ICON FAIL: ${evt.classname} :: ${evt.message ?? ""}`);
|
|
}
|
|
if (evt.type === "error") log(`ICON ERROR: ${evt.message ?? ""}`);
|
|
});
|
|
log("icon result: " + JSON.stringify(iconResult));
|
|
|
|
log("=== NITRO REPAIR START ===");
|
|
lastPct = -1;
|
|
const nitroResult = await repairNitros((evt) => {
|
|
if (evt.type === "progress" && evt.total) {
|
|
const pct = Math.round(((evt.index ?? 0) / evt.total) * 100);
|
|
if (pct % 5 === 0 && pct !== lastPct) {
|
|
lastPct = pct;
|
|
log(`nitros ${evt.index}/${evt.total} (${pct}%)`);
|
|
}
|
|
if (evt.status === "done") log(`NITRO DONE: ${evt.classname}`);
|
|
if (evt.status === "failed")
|
|
log(`NITRO FAIL: ${evt.classname} :: ${evt.message ?? ""}`);
|
|
}
|
|
if (evt.type === "error") log(`NITRO ERROR: ${evt.message ?? ""}`);
|
|
});
|
|
log("nitro result: " + JSON.stringify(nitroResult));
|
|
|
|
log("=== ASSET REPAIR COMPLETE ===");
|
|
log("icons: " + JSON.stringify(iconResult));
|
|
log("nitros: " + JSON.stringify(nitroResult));
|
|
}, 1800000);
|
|
});
|