This commit is contained in:
1 parent
8efd032cc6
commit
df38dccbf1
735 files changed
+128321
-120870
No files matched your search
@@ -4,10 +4,10 @@ import { apiOk } from "@/lib/api-response";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import {
|
||||
appendFurniEntriesBatch,
|
||||
buildFurniEntry,
|
||||
getFurnitureDataPath,
|
||||
readFurniData,
|
||||
appendFurniEntriesBatch,
|
||||
buildFurniEntry,
|
||||
getFurnitureDataPath,
|
||||
readFurniData,
|
||||
} from "@/lib/services/furni-data";
|
||||
import { lookupHabboIt } from "@/lib/services/habbo-furnidata-cache";
|
||||
import { rcon } from "@/lib/services/rcon";
|
||||
@@ -26,171 +26,201 @@ import { rcon } from "@/lib/services/rcon";
|
||||
* Entries are upserted by classname so running this repeatedly is safe.
|
||||
* Triggers RCON updateCatalog + updateItems at the end.
|
||||
*/
|
||||
export const POST = withAdmin({ permission: PERMS.ASSETS_IMPORT }, async (request) => {
|
||||
const qp = request.nextUrl.searchParams;
|
||||
const mode: "days" | "missing" | "broken" | "all" =
|
||||
qp.get("all") === "1"
|
||||
? "all"
|
||||
: qp.get("broken") === "1"
|
||||
? "broken"
|
||||
: qp.get("missing") === "1"
|
||||
? "missing"
|
||||
: "days";
|
||||
export const POST = withAdmin(
|
||||
{ permission: PERMS.ASSETS_IMPORT },
|
||||
async (request) => {
|
||||
const qp = request.nextUrl.searchParams;
|
||||
const mode: "days" | "missing" | "broken" | "all" =
|
||||
qp.get("all") === "1"
|
||||
? "all"
|
||||
: qp.get("broken") === "1"
|
||||
? "broken"
|
||||
: qp.get("missing") === "1"
|
||||
? "missing"
|
||||
: "days";
|
||||
|
||||
const days = Math.max(1, Math.min(parseInt(qp.get("days") || "7", 10) || 7, 365));
|
||||
const days = Math.max(
|
||||
1,
|
||||
Math.min(parseInt(qp.get("days") || "7", 10) || 7, 365),
|
||||
);
|
||||
|
||||
const targetIds = await resolveTargetIds(mode, days);
|
||||
const targetIds = await resolveTargetIds(mode, days);
|
||||
|
||||
if (targetIds.length === 0) {
|
||||
return apiOk({
|
||||
mode,
|
||||
days: mode === "days" ? days : undefined,
|
||||
examined: 0,
|
||||
resynced: 0,
|
||||
failed: 0,
|
||||
errors: [],
|
||||
});
|
||||
}
|
||||
if (targetIds.length === 0) {
|
||||
return apiOk({
|
||||
mode,
|
||||
days: mode === "days" ? days : undefined,
|
||||
examined: 0,
|
||||
resynced: 0,
|
||||
failed: 0,
|
||||
errors: [],
|
||||
});
|
||||
}
|
||||
|
||||
// Process in chunks to keep memory flat and make progress visible.
|
||||
const CHUNK = 500;
|
||||
let resynced = 0;
|
||||
const errors: Array<{ classname: string; message: string }> = [];
|
||||
// Process in chunks to keep memory flat and make progress visible.
|
||||
const CHUNK = 500;
|
||||
let resynced = 0;
|
||||
const errors: Array<{ classname: string; message: string }> = [];
|
||||
|
||||
for (let i = 0; i < targetIds.length; i += CHUNK) {
|
||||
const slice = targetIds.slice(i, i + CHUNK);
|
||||
const rows = await prisma.itemsBase.findMany({
|
||||
where: { id: { in: slice } },
|
||||
select: {
|
||||
id: true,
|
||||
spriteId: true,
|
||||
itemName: true,
|
||||
publicName: true,
|
||||
type: true,
|
||||
width: true,
|
||||
length: true,
|
||||
stackHeight: true,
|
||||
},
|
||||
});
|
||||
for (let i = 0; i < targetIds.length; i += CHUNK) {
|
||||
const slice = targetIds.slice(i, i + CHUNK);
|
||||
const rows = await prisma.itemsBase.findMany({
|
||||
where: { id: { in: slice } },
|
||||
select: {
|
||||
id: true,
|
||||
spriteId: true,
|
||||
itemName: true,
|
||||
publicName: true,
|
||||
type: true,
|
||||
width: true,
|
||||
length: true,
|
||||
stackHeight: true,
|
||||
},
|
||||
});
|
||||
|
||||
const entries: Array<{ entry: Record<string, unknown>; itemType: string }> = [];
|
||||
for (const row of rows) {
|
||||
try {
|
||||
const classname = row.itemName;
|
||||
let habboIt = null;
|
||||
try {
|
||||
habboIt = await lookupHabboIt(classname);
|
||||
} catch {
|
||||
/* optional */
|
||||
}
|
||||
const entries: Array<{
|
||||
entry: Record<string, unknown>;
|
||||
itemType: string;
|
||||
}> = [];
|
||||
for (const row of rows) {
|
||||
try {
|
||||
const classname = row.itemName;
|
||||
let habboIt = null;
|
||||
try {
|
||||
habboIt = await lookupHabboIt(classname);
|
||||
} catch {
|
||||
/* optional */
|
||||
}
|
||||
|
||||
const entry = buildFurniEntry({
|
||||
id: row.id,
|
||||
classname,
|
||||
revision: habboIt?.revision ?? 0,
|
||||
category: habboIt?.category || "unknown",
|
||||
name: row.publicName || habboIt?.name || classname,
|
||||
description: habboIt?.description || "",
|
||||
spriteId: row.spriteId,
|
||||
dims: { x: row.width, y: row.length, z: Math.round(Number(row.stackHeight)) },
|
||||
metadata: null,
|
||||
habboIt,
|
||||
});
|
||||
const entry = buildFurniEntry({
|
||||
id: row.id,
|
||||
classname,
|
||||
revision: habboIt?.revision ?? 0,
|
||||
category: habboIt?.category || "unknown",
|
||||
name: row.publicName || habboIt?.name || classname,
|
||||
description: habboIt?.description || "",
|
||||
spriteId: row.spriteId,
|
||||
dims: {
|
||||
x: row.width,
|
||||
y: row.length,
|
||||
z: Math.round(Number(row.stackHeight)),
|
||||
},
|
||||
metadata: null,
|
||||
habboIt,
|
||||
});
|
||||
|
||||
entries.push({ entry, itemType: row.type === "i" ? "i" : "s" });
|
||||
} catch (err) {
|
||||
errors.push({ classname: row.itemName, message: (err as Error).message });
|
||||
}
|
||||
}
|
||||
entries.push({ entry, itemType: row.type === "i" ? "i" : "s" });
|
||||
} catch (err) {
|
||||
errors.push({
|
||||
classname: row.itemName,
|
||||
message: (err as Error).message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (entries.length > 0) {
|
||||
try {
|
||||
await appendFurniEntriesBatch(entries);
|
||||
resynced += entries.length;
|
||||
} catch (err) {
|
||||
errors.push({
|
||||
classname: `_chunk_${i}_${i + slice.length}_`,
|
||||
message: `FurnitureData.json write failed: ${(err as Error).message}`,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (entries.length > 0) {
|
||||
try {
|
||||
await appendFurniEntriesBatch(entries);
|
||||
resynced += entries.length;
|
||||
} catch (err) {
|
||||
errors.push({
|
||||
classname: `_chunk_${i}_${i + slice.length}_`,
|
||||
message: `FurnitureData.json write failed: ${(err as Error).message}`,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let rconOk = false;
|
||||
try {
|
||||
const a = await rcon.updateCatalog();
|
||||
const b = await rcon.updateItems();
|
||||
rconOk = a && b;
|
||||
} catch {
|
||||
/* logged in rcon service */
|
||||
}
|
||||
let rconOk = false;
|
||||
try {
|
||||
const a = await rcon.updateCatalog();
|
||||
const b = await rcon.updateItems();
|
||||
rconOk = a && b;
|
||||
} catch {
|
||||
/* logged in rcon service */
|
||||
}
|
||||
|
||||
return apiOk({
|
||||
mode,
|
||||
days: mode === "days" ? days : undefined,
|
||||
examined: targetIds.length,
|
||||
resynced,
|
||||
failed: errors.length,
|
||||
rconOk,
|
||||
errors: errors.slice(0, 50),
|
||||
});
|
||||
});
|
||||
return apiOk({
|
||||
mode,
|
||||
days: mode === "days" ? days : undefined,
|
||||
examined: targetIds.length,
|
||||
resynced,
|
||||
failed: errors.length,
|
||||
rconOk,
|
||||
errors: errors.slice(0, 50),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
async function resolveTargetIds(
|
||||
mode: "days" | "missing" | "broken" | "all",
|
||||
days: number,
|
||||
mode: "days" | "missing" | "broken" | "all",
|
||||
days: number,
|
||||
): Promise<number[]> {
|
||||
if (mode === "all") {
|
||||
const rows = await prisma.itemsBase.findMany({ select: { id: true }, orderBy: { id: "asc" } });
|
||||
return rows.map((r) => r.id);
|
||||
}
|
||||
if (mode === "all") {
|
||||
const rows = await prisma.itemsBase.findMany({
|
||||
select: { id: true },
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
return rows.map((r) => r.id);
|
||||
}
|
||||
|
||||
if (mode === "missing" || mode === "broken") {
|
||||
const furniDataPath = await getFurnitureDataPath();
|
||||
if (!existsSync(furniDataPath)) {
|
||||
const rows = await prisma.itemsBase.findMany({ select: { id: true }, orderBy: { id: "asc" } });
|
||||
return rows.map((r) => r.id);
|
||||
}
|
||||
const furniData = (await readFurniData()) as {
|
||||
roomitemtypes?: { furnitype: Array<Record<string, unknown>> };
|
||||
wallitemtypes?: { furnitype: Array<Record<string, unknown>> };
|
||||
};
|
||||
const byClassname = new Map<string, Record<string, unknown>>();
|
||||
for (const section of [furniData.roomitemtypes, furniData.wallitemtypes]) {
|
||||
for (const e of section?.furnitype ?? []) {
|
||||
if (typeof e?.classname === "string") byClassname.set(e.classname, e);
|
||||
}
|
||||
}
|
||||
const rows = await prisma.itemsBase.findMany({
|
||||
select: { id: true, itemName: true, spriteId: true, width: true, length: true },
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
if (mode === "missing" || mode === "broken") {
|
||||
const furniDataPath = await getFurnitureDataPath();
|
||||
if (!existsSync(furniDataPath)) {
|
||||
const rows = await prisma.itemsBase.findMany({
|
||||
select: { id: true },
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
return rows.map((r) => r.id);
|
||||
}
|
||||
const furniData = (await readFurniData()) as {
|
||||
roomitemtypes?: { furnitype: Array<Record<string, unknown>> };
|
||||
wallitemtypes?: { furnitype: Array<Record<string, unknown>> };
|
||||
};
|
||||
const byClassname = new Map<string, Record<string, unknown>>();
|
||||
for (const section of [furniData.roomitemtypes, furniData.wallitemtypes]) {
|
||||
for (const e of section?.furnitype ?? []) {
|
||||
if (typeof e?.classname === "string") byClassname.set(e.classname, e);
|
||||
}
|
||||
}
|
||||
const rows = await prisma.itemsBase.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
itemName: true,
|
||||
spriteId: true,
|
||||
width: true,
|
||||
length: true,
|
||||
},
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
|
||||
if (mode === "missing") {
|
||||
return rows.filter((r) => !byClassname.has(r.itemName)).map((r) => r.id);
|
||||
}
|
||||
if (mode === "missing") {
|
||||
return rows.filter((r) => !byClassname.has(r.itemName)).map((r) => r.id);
|
||||
}
|
||||
|
||||
// broken: entry present but inconsistent with items_base
|
||||
return rows
|
||||
.filter((r) => {
|
||||
const e = byClassname.get(r.itemName);
|
||||
if (!e) return true;
|
||||
const entryId = Number(e.id ?? -1);
|
||||
if (entryId !== r.id && entryId !== r.spriteId) return true;
|
||||
const xdim = Number(e.xdim ?? 0);
|
||||
const ydim = Number(e.ydim ?? 0);
|
||||
if (xdim <= 0 || ydim <= 0) return true;
|
||||
if (xdim !== r.width || ydim !== r.length) return true;
|
||||
return false;
|
||||
})
|
||||
.map((r) => r.id);
|
||||
}
|
||||
// broken: entry present but inconsistent with items_base
|
||||
return rows
|
||||
.filter((r) => {
|
||||
const e = byClassname.get(r.itemName);
|
||||
if (!e) return true;
|
||||
const entryId = Number(e.id ?? -1);
|
||||
if (entryId !== r.id && entryId !== r.spriteId) return true;
|
||||
const xdim = Number(e.xdim ?? 0);
|
||||
const ydim = Number(e.ydim ?? 0);
|
||||
if (xdim <= 0 || ydim <= 0) return true;
|
||||
if (xdim !== r.width || ydim !== r.length) return true;
|
||||
return false;
|
||||
})
|
||||
.map((r) => r.id);
|
||||
}
|
||||
|
||||
// mode === 'days'
|
||||
const sinceIso = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
||||
const sinceMysql = sinceIso.slice(0, 19).replace("T", " ");
|
||||
const auditRows = await prisma.$queryRaw<Array<{ target_id: number }>>`
|
||||
// mode === 'days'
|
||||
const sinceIso = new Date(
|
||||
Date.now() - days * 24 * 60 * 60 * 1000,
|
||||
).toISOString();
|
||||
const sinceMysql = sinceIso.slice(0, 19).replace("T", " ");
|
||||
const auditRows = await prisma.$queryRaw<Array<{ target_id: number }>>`
|
||||
SELECT DISTINCT target_id
|
||||
FROM admin_audit_log
|
||||
WHERE action = 'furni_import'
|
||||
@@ -199,5 +229,5 @@ async function resolveTargetIds(
|
||||
AND created_at >= ${sinceMysql}
|
||||
ORDER BY target_id ASC
|
||||
`;
|
||||
return auditRows.map((r) => r.target_id).filter(Boolean);
|
||||
return auditRows.map((r) => r.target_id).filter(Boolean);
|
||||
}
|
||||
Reference in new issue
Block a user