feat(catalog): prevent and repair Arcturus emulator data errors
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.
This commit is contained in:
1 parent
ad65d80d41
commit
3e69b72513
26 files changed
+17651
-3844
No files matched your search
@@ -0,0 +1,66 @@
|
||||
// Direct repair execution against the live DB. Skips the slow clone-source
|
||||
// comparison entirely and just runs the repair functions.
|
||||
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const LOG = "/tmp/catalog-repair.log";
|
||||
const log = (msg: string) => {
|
||||
appendFileSync(LOG, `${msg}\n`);
|
||||
console.log(msg);
|
||||
};
|
||||
|
||||
import { sql } from "drizzle-orm";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
|
||||
const runLive = process.env.RUN_CATALOG_AUDIT_LIVE === "1";
|
||||
|
||||
describe.skipIf(!runLive)("catalog repair direct (live)", () => {
|
||||
let db: typeof import("@/lib/db").db;
|
||||
let repair: typeof import("@/lib/services/catalog-repair");
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const [dbMod, repairMod] = await Promise.all([
|
||||
import("@/lib/db"),
|
||||
import("@/lib/services/catalog-repair"),
|
||||
]);
|
||||
db = dbMod.db;
|
||||
repair = repairMod;
|
||||
});
|
||||
|
||||
it("repairs orphaned catalog + duplicate classnames", async () => {
|
||||
expect(process.env.DATABASE_URL).toMatch(/@/);
|
||||
|
||||
log("=== ORPHANED REPAIR ===");
|
||||
const orphan = await repair.repairOrphanedCatalog();
|
||||
log("orphaned result: " + JSON.stringify(orphan));
|
||||
|
||||
log("=== DUPLICATE REPAIR ===");
|
||||
const dup = await repair.repairDuplicateClassnames();
|
||||
log("duplicate result: " + JSON.stringify(dup));
|
||||
|
||||
log("=== VERIFY DUPLICATES ===");
|
||||
const [dupRows] = (await db.execute(sql`
|
||||
SELECT item_name, COUNT(*) as cnt
|
||||
FROM items_base
|
||||
GROUP BY item_name
|
||||
HAVING cnt > 1
|
||||
`)) as unknown as [Array<{ item_name: string; cnt: number }>, unknown];
|
||||
log("remaining duplicate groups: " + JSON.stringify(dupRows));
|
||||
}, 300000);
|
||||
});
|
||||
Reference in new issue
Block a user