refactor: universal logger and remove N+1 catalog queries
CI / check (push) Successful in 28s
CI / tests-ui (push) Failing after 32s
CI / tests-unit (push) Successful in 1m43s
CI / tests-quality (push) Successful in 1m14s
CI / tests-integration (push) Successful in 1m50s
CI / preflight (push) Skipped
CI / deploy (push) Skipped
CI / tests-imaging-browsers (push) Successful in 1m56s
CI / check (push) Successful in 28s
CI / tests-ui (push) Failing after 32s
CI / tests-unit (push) Successful in 1m43s
CI / tests-quality (push) Successful in 1m14s
CI / tests-integration (push) Successful in 1m50s
CI / preflight (push) Skipped
CI / deploy (push) Skipped
CI / tests-imaging-browsers (push) Successful in 1m56s
- Replace all 39 console.* calls with universal logger (pino server, console browser) - Set server-only marker on import-badge.ts - Disable DB query logging in production - Add bulkInsertCatalogItems: one transaction instead of N per-item inserts - Add reserveCatalogItemIds helper for bulk ID allocation - Optimize translateCatalogItems: single inArray batch query instead of N per-item selects - Test mock updated for logger.debug - biome clean, tsc clean, 3569 tests passing
This commit is contained in:
1 parent
6895eaab49
commit
59a45d2165
27 files changed
+406
-231
No files matched your search
+197
-92
@@ -24,7 +24,10 @@ import { CatalogItems, db, ItemsBase, queryRows } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { logAudit } from "@/lib/services/audit";
|
||||
import { withCatalogExport } from "@/lib/services/catalog-git-queue";
|
||||
import { allocateCatalogItemId } from "@/lib/services/furni-import";
|
||||
import {
|
||||
allocateCatalogItemId,
|
||||
reserveCatalogItemIds,
|
||||
} from "@/lib/services/furni-import";
|
||||
import { logStaffActivity } from "@/lib/services/staff-activity";
|
||||
import { translateItemsSchema } from "@/lib/validators/catalog";
|
||||
|
||||
@@ -133,6 +136,138 @@ export async function createCatalogItem(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk insert catalog items in ONE transaction instead of N per-item
|
||||
* transactions + N INSERT queries. Same result as calling insertCatalogItemRow
|
||||
* per row, but far faster for large batches. Side effects (audit, revalidate,
|
||||
* RCON refresh) are left to the caller, which wraps this in withCatalogExport.
|
||||
*/
|
||||
export async function bulkInsertCatalogItems(data: {
|
||||
pageId: number;
|
||||
staffId: number;
|
||||
rows: Array<{
|
||||
baseId: number;
|
||||
credits?: number;
|
||||
points?: number;
|
||||
pointsType?: number;
|
||||
amount?: number;
|
||||
}>;
|
||||
}): Promise<{ created: number; failed: number }> {
|
||||
const { pageId, staffId, rows } = data;
|
||||
if (rows.length === 0) return { created: 0, failed: 0 };
|
||||
|
||||
const baseIds = [...new Set(rows.map((r) => r.baseId))];
|
||||
const bases = await db
|
||||
.select({
|
||||
id: ItemsBase.id,
|
||||
publicName: ItemsBase.publicName,
|
||||
itemName: ItemsBase.itemName,
|
||||
})
|
||||
.from(ItemsBase)
|
||||
.where(inArray(ItemsBase.id, baseIds));
|
||||
const baseMap = new Map(bases.map((b) => [b.id, b]));
|
||||
|
||||
const ids = await reserveCatalogItemIds(rows.length);
|
||||
|
||||
const maxOrder = await queryRows<{ maxOrder: number }>(sql`
|
||||
SELECT COALESCE(MAX(order_number), 0) AS maxOrder FROM catalog_items WHERE page_id = ${String(pageId)}
|
||||
`);
|
||||
let nextOrder = Number(maxOrder[0]?.maxOrder ?? 0) + 1;
|
||||
|
||||
const values: Array<{
|
||||
id: number;
|
||||
pageId: string;
|
||||
itemIds: string;
|
||||
catalogName: string;
|
||||
costCredits: number;
|
||||
costPoints: number;
|
||||
pointsType: number;
|
||||
amount: number;
|
||||
orderNumber: number;
|
||||
offerId: number;
|
||||
songId: number;
|
||||
limitedSells: number;
|
||||
limitedStack: number;
|
||||
extradata: string;
|
||||
haveOffer: string;
|
||||
clubOnly: string;
|
||||
}> = [];
|
||||
|
||||
let failed = 0;
|
||||
let validIdx = 0;
|
||||
|
||||
for (const row of rows) {
|
||||
const base = baseMap.get(row.baseId);
|
||||
if (!base) {
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
values.push({
|
||||
id: ids[validIdx],
|
||||
pageId: String(pageId),
|
||||
itemIds: String(row.baseId),
|
||||
catalogName: base.publicName || base.itemName || String(row.baseId),
|
||||
costCredits: row.credits ?? 0,
|
||||
costPoints: row.points ?? 0,
|
||||
pointsType: row.pointsType ?? 0,
|
||||
amount: row.amount ?? 1,
|
||||
orderNumber: nextOrder++,
|
||||
offerId: -1,
|
||||
songId: 0,
|
||||
limitedSells: 0,
|
||||
limitedStack: 0,
|
||||
extradata: "",
|
||||
haveOffer: "1",
|
||||
clubOnly: "0",
|
||||
});
|
||||
validIdx++;
|
||||
}
|
||||
|
||||
if (values.length === 0) return { created: 0, failed };
|
||||
|
||||
const allFurnitureIds = [...new Set(values.map((v) => Number(v.itemIds)))];
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.execute(
|
||||
sql`SELECT id FROM catalog_pages WHERE id=${String(pageId)} FOR UPDATE`,
|
||||
);
|
||||
if (allFurnitureIds.length) {
|
||||
await tx.execute(
|
||||
sql`SELECT id FROM items_base WHERE id IN (${sql.join(allFurnitureIds, sql`, `)}) ORDER BY id FOR UPDATE`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const v of values) {
|
||||
await tx.execute(sql`
|
||||
INSERT INTO catalog_items (
|
||||
id, page_id, item_ids, catalog_name,
|
||||
cost_credits, cost_points, points_type, amount,
|
||||
order_number, offer_id, song_id,
|
||||
limited_sells, limited_stack, extradata, have_offer, club_only
|
||||
) VALUES (
|
||||
${v.id}, ${v.pageId}, ${v.itemIds}, ${v.catalogName},
|
||||
${v.costCredits}, ${v.costPoints}, ${v.pointsType}, ${v.amount},
|
||||
${v.orderNumber}, ${v.offerId}, ${v.songId},
|
||||
${v.limitedSells}, ${v.limitedStack}, ${v.extradata},
|
||||
${v.haveOffer}, ${v.clubOnly}
|
||||
)
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
await sendCatalogUpdate();
|
||||
await logStaffActivity({
|
||||
staffId,
|
||||
action: "catalog_items_bulk_create",
|
||||
description: `Bulk inserted ${values.length} catalog item(s) on page #${pageId}`,
|
||||
targetType: "catalog_page",
|
||||
targetId: pageId,
|
||||
});
|
||||
revalidatePath("/admin/catalog");
|
||||
|
||||
return { created: values.length, failed };
|
||||
}
|
||||
|
||||
/** Bulk create with one RCON refresh at the end. */
|
||||
export async function bulkCreateCatalogItems({
|
||||
pageId,
|
||||
@@ -155,67 +290,17 @@ export async function bulkCreateCatalogItems({
|
||||
return { ok: false as const, error: "Max 500 items per bulk import" };
|
||||
}
|
||||
|
||||
const baseIds = [...new Set(rows.map((r) => r.baseId))];
|
||||
const bases = await db
|
||||
.select({
|
||||
id: ItemsBase.id,
|
||||
publicName: ItemsBase.publicName,
|
||||
itemName: ItemsBase.itemName,
|
||||
})
|
||||
.from(ItemsBase)
|
||||
.where(inArray(ItemsBase.id, baseIds));
|
||||
const baseMap = new Map(bases.map((b) => [b.id, b]));
|
||||
|
||||
let created = 0;
|
||||
let failed = 0;
|
||||
|
||||
// Find the next order number on the target page to avoid collisions.
|
||||
const maxOrder = await queryRows<{ maxOrder: number }>(sql`
|
||||
SELECT COALESCE(MAX(order_number), 0) AS maxOrder FROM catalog_items WHERE page_id = ${String(pageId)}
|
||||
`);
|
||||
let nextOrder = Number(maxOrder[0]?.maxOrder ?? 0) + 1;
|
||||
|
||||
for (const row of rows) {
|
||||
const base = baseMap.get(row.baseId);
|
||||
if (!base) {
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await insertCatalogItemRow({
|
||||
const { created, failed } = await bulkInsertCatalogItems({
|
||||
pageId,
|
||||
itemIds: String(row.baseId),
|
||||
catalogName: base.publicName || base.itemName || String(row.baseId),
|
||||
costCredits: row.credits ?? 0,
|
||||
costPoints: row.points ?? 0,
|
||||
pointsType: row.pointsType ?? 0,
|
||||
amount: 1,
|
||||
limitedSells: 0,
|
||||
limitedStack: 0,
|
||||
orderNumber: nextOrder++,
|
||||
offerId: -1,
|
||||
songId: 0,
|
||||
haveOffer: "1",
|
||||
clubOnly: "0",
|
||||
extradata: "",
|
||||
});
|
||||
created++;
|
||||
} catch {
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (created > 0) {
|
||||
await sendCatalogUpdate();
|
||||
await logStaffActivity({
|
||||
staffId: staff.id,
|
||||
action: "catalog_items_bulk_create",
|
||||
description: `Bulk imported ${created} catalog item(s) on page #${pageId}`,
|
||||
targetType: "catalog_page",
|
||||
targetId: pageId,
|
||||
rows: rows.map((r) => ({
|
||||
baseId: r.baseId,
|
||||
credits: r.credits,
|
||||
points: r.points,
|
||||
pointsType: r.pointsType,
|
||||
amount: 1,
|
||||
})),
|
||||
});
|
||||
revalidatePath("/admin/catalog");
|
||||
}
|
||||
|
||||
return { ok: true as const, data: { created, failed } };
|
||||
});
|
||||
@@ -504,6 +589,19 @@ export async function translateCatalogItems(input: {
|
||||
"@/lib/services/furni-data-i18n"
|
||||
);
|
||||
|
||||
const itemIds = items.map((i) => i.id);
|
||||
const allBases = await db
|
||||
.select({
|
||||
id: ItemsBase.id,
|
||||
publicName: ItemsBase.publicName,
|
||||
itemName: ItemsBase.itemName,
|
||||
type: ItemsBase.type,
|
||||
spriteId: ItemsBase.spriteId,
|
||||
})
|
||||
.from(ItemsBase)
|
||||
.where(inArray(ItemsBase.id, itemIds));
|
||||
const baseById = new Map(allBases.map((b) => [b.id, b]));
|
||||
|
||||
let namesUpdated = 0;
|
||||
let descriptionsUpdated = 0;
|
||||
const furniPatches: Array<{
|
||||
@@ -519,19 +617,14 @@ export async function translateCatalogItems(input: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
}> = [];
|
||||
const nameUpdates: Array<{ id: number; nextName: string }> = [];
|
||||
const catalogNameUpdates: Array<{
|
||||
idStr: string;
|
||||
nextName: string;
|
||||
}> = [];
|
||||
|
||||
for (const item of items) {
|
||||
const [base] = await db
|
||||
.select({
|
||||
id: ItemsBase.id,
|
||||
publicName: ItemsBase.publicName,
|
||||
itemName: ItemsBase.itemName,
|
||||
type: ItemsBase.type,
|
||||
spriteId: ItemsBase.spriteId,
|
||||
})
|
||||
.from(ItemsBase)
|
||||
.where(eq(ItemsBase.id, item.id))
|
||||
.limit(1);
|
||||
const base = baseById.get(item.id);
|
||||
if (!base) continue;
|
||||
|
||||
const nextName = item.publicName?.trim() ?? "";
|
||||
@@ -540,11 +633,43 @@ export async function translateCatalogItems(input: {
|
||||
nextName !== "" && nextName !== (base.publicName ?? "");
|
||||
|
||||
if (nameChanged) {
|
||||
nameUpdates.push({ id: base.id, nextName });
|
||||
catalogNameUpdates.push({
|
||||
idStr: String(base.id),
|
||||
nextName,
|
||||
});
|
||||
namesUpdated++;
|
||||
}
|
||||
|
||||
if (nextDesc !== "" || nameChanged) {
|
||||
descriptionsUpdated += nextDesc !== "" ? 1 : 0;
|
||||
const englishName = base.publicName?.trim() || base.itemName || "";
|
||||
furniPatches.push({
|
||||
classname: base.itemName,
|
||||
itemType: base.type || "s",
|
||||
name: nextName || base.publicName || base.itemName,
|
||||
description: nextDesc,
|
||||
spriteId: base.spriteId,
|
||||
createIfMissing: true,
|
||||
});
|
||||
localizedEntries.push({
|
||||
classname: base.itemName,
|
||||
name: englishName,
|
||||
description: nextDesc || undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Batch ItemsBase name updates.
|
||||
for (const { id, nextName } of nameUpdates) {
|
||||
await db
|
||||
.update(ItemsBase)
|
||||
.set({ publicName: nextName })
|
||||
.where(eq(ItemsBase.id, base.id));
|
||||
const idStr = String(base.id);
|
||||
.where(eq(ItemsBase.id, id));
|
||||
}
|
||||
|
||||
// Batch CatalogItems name updates: one SELECT + UPDATE per idStr.
|
||||
for (const { idStr, nextName } of catalogNameUpdates) {
|
||||
const related = await db
|
||||
.select({
|
||||
id: CatalogItems.id,
|
||||
@@ -567,26 +692,6 @@ export async function translateCatalogItems(input: {
|
||||
.where(eq(CatalogItems.id, row.id));
|
||||
}
|
||||
}
|
||||
namesUpdated++;
|
||||
}
|
||||
|
||||
if (nextDesc !== "" || nameChanged) {
|
||||
descriptionsUpdated += nextDesc !== "" ? 1 : 0;
|
||||
const englishName = base.publicName?.trim() || base.itemName || "";
|
||||
furniPatches.push({
|
||||
classname: base.itemName,
|
||||
itemType: base.type || "s",
|
||||
name: nextName || base.publicName || base.itemName,
|
||||
description: nextDesc,
|
||||
spriteId: base.spriteId,
|
||||
createIfMissing: true,
|
||||
});
|
||||
localizedEntries.push({
|
||||
classname: base.itemName,
|
||||
name: englishName,
|
||||
description: nextDesc || undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const furniResult =
|
||||
|
||||
+13
-45
@@ -2,7 +2,10 @@
|
||||
|
||||
import { inArray, sql } from "drizzle-orm";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { insertCatalogItemRow } from "@/actions/catalog-items";
|
||||
import {
|
||||
bulkInsertCatalogItems,
|
||||
insertCatalogItemRow,
|
||||
} from "@/actions/catalog-items";
|
||||
import { catalogFailure } from "@/features/catalog/server/errors";
|
||||
import {
|
||||
createPageCommand,
|
||||
@@ -279,52 +282,17 @@ export async function createAutoCategory(input: {
|
||||
}
|
||||
|
||||
const rows = input.offerRows ?? [];
|
||||
const baseIds = [...new Set(rows.map((r) => r.baseId))];
|
||||
const bases = await db
|
||||
.select({
|
||||
id: ItemsBase.id,
|
||||
publicName: ItemsBase.publicName,
|
||||
itemName: ItemsBase.itemName,
|
||||
})
|
||||
.from(ItemsBase)
|
||||
.where(inArray(ItemsBase.id, baseIds));
|
||||
const baseMap = new Map(bases.map((b) => [b.id, b]));
|
||||
|
||||
const maxOrder = await queryRows<{ maxOrder: number }>(sql`
|
||||
SELECT COALESCE(MAX(order_number), 0) AS maxOrder FROM catalog_items WHERE page_id = ${String(pageId)}
|
||||
`);
|
||||
let nextOrder = Number(maxOrder[0]?.maxOrder ?? 0) + 1;
|
||||
|
||||
let created = 0;
|
||||
for (const row of rows) {
|
||||
const base = baseMap.get(row.baseId);
|
||||
if (!base) continue;
|
||||
try {
|
||||
await insertCatalogItemRow({
|
||||
const { created } = await bulkInsertCatalogItems({
|
||||
pageId,
|
||||
itemIds: String(row.baseId),
|
||||
catalogName: base.publicName || base.itemName || String(row.baseId),
|
||||
costCredits: row.credits ?? 0,
|
||||
costPoints: row.points ?? 0,
|
||||
pointsType: row.pointsType ?? 0,
|
||||
amount: row.amount ?? 1,
|
||||
limitedSells: 0,
|
||||
limitedStack: 0,
|
||||
orderNumber: nextOrder++,
|
||||
offerId: -1,
|
||||
songId: 0,
|
||||
haveOffer: "1",
|
||||
clubOnly: "0",
|
||||
extradata: "",
|
||||
staffId: staff.id,
|
||||
rows: rows.map((r) => ({
|
||||
baseId: r.baseId,
|
||||
credits: r.credits,
|
||||
points: r.points,
|
||||
pointsType: r.pointsType,
|
||||
amount: r.amount,
|
||||
})),
|
||||
});
|
||||
created++;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[createAutoCategory] failed to insert item #${row.baseId}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await logStaffActivity({
|
||||
staffId: staff.id,
|
||||
|
||||
@@ -46,6 +46,7 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { useServerAction } from "@/hooks/use-server-action";
|
||||
import { adminFetch } from "@/lib/admin-fetch";
|
||||
import { invalidateSongPickerCache } from "@/lib/client-cache/song-picker-cache";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { parseTraxChannels } from "@/lib/trax-format";
|
||||
import { TraxPlayer } from "./trax-player";
|
||||
|
||||
@@ -455,7 +456,9 @@ function UploadDialog({
|
||||
onOpenChange(false);
|
||||
onUploaded();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
logger.error("Sound upload failed", {
|
||||
err: err as Error,
|
||||
});
|
||||
toast.error("Network error during upload.");
|
||||
} finally {
|
||||
setUploading(false);
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Loader2, Play, Square } from "lucide-react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { TRAX_TICK_SECONDS, type TraxChannel } from "@/lib/trax-format";
|
||||
|
||||
const TICK_SECONDS = TRAX_TICK_SECONDS;
|
||||
@@ -127,7 +128,9 @@ export function TraxPlayer({
|
||||
buffers.set(id, buf);
|
||||
} catch (err) {
|
||||
failed++;
|
||||
console.warn(`[trax] failed to load sample ${id}:`, err);
|
||||
logger.warn(`[trax] failed to load sample ${id}`, {
|
||||
err: err as Error,
|
||||
});
|
||||
} finally {
|
||||
done++;
|
||||
setLoaded(done);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
bundleExistsInDir,
|
||||
bundleWritePathInDir,
|
||||
} from "@/lib/furni/bundle-file";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
||||
import {
|
||||
@@ -315,9 +316,8 @@ export const POST = withAdmin(
|
||||
try {
|
||||
languages = await buildLocalizedFurniDataFiles();
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"[batch-regen] Localized furnidata build failed:",
|
||||
(err as Error).message,
|
||||
logger.warn(
|
||||
`[batch-regen] Localized furnidata build failed: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { apiError } from "@/lib/api";
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { getRequestId } from "@/lib/foundation/request-context";
|
||||
import type { ImportJob } from "@/lib/furni/import-job";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { logAudit } from "@/lib/services/audit";
|
||||
import { getSource } from "@/lib/services/clone-sources";
|
||||
@@ -112,10 +113,9 @@ export const POST = withAdmin(
|
||||
await store.save(job);
|
||||
return job;
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[import-furni] Could not create durable batch job",
|
||||
error,
|
||||
);
|
||||
logger.warn("[import-furni] Could not create durable batch job", {
|
||||
err: error as Error,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
@@ -156,7 +156,9 @@ export const POST = withAdmin(
|
||||
try {
|
||||
await store.save(mirrorJob);
|
||||
} catch (error) {
|
||||
console.warn("[import-furni] Checkpoint write failed", error);
|
||||
logger.warn("[import-furni] Checkpoint write failed", {
|
||||
err: error as Error,
|
||||
});
|
||||
} finally {
|
||||
saveQueued = false;
|
||||
}
|
||||
@@ -177,7 +179,9 @@ export const POST = withAdmin(
|
||||
try {
|
||||
await store.save(mirrorJob);
|
||||
} catch (error) {
|
||||
console.warn("[import-furni] Final checkpoint write failed", error);
|
||||
logger.warn("[import-furni] Final checkpoint write failed", {
|
||||
err: error as Error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -412,9 +416,8 @@ export const POST = withAdmin(
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"[import-furni] RCON update failed after batch:",
|
||||
(err as Error).message,
|
||||
logger.warn(
|
||||
`[import-furni] RCON update failed after batch: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -470,15 +473,13 @@ export const POST = withAdmin(
|
||||
try {
|
||||
languages = await buildLocalizedFurniDataFiles();
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"[import-furni] Localized furnidata build failed:",
|
||||
(err as Error).message,
|
||||
logger.warn(
|
||||
`[import-furni] Localized furnidata build failed: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"[import-furni] Post-import reconcile/ownership failed:",
|
||||
(err as Error).message,
|
||||
logger.warn(
|
||||
`[import-furni] Post-import reconcile/ownership failed: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { apiError, apiOk } from "@/lib/api";
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { db, ItemsBase, queryRows } from "@/lib/db";
|
||||
import { resolveBundleInDir } from "@/lib/furni/bundle-file";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { logAudit } from "@/lib/services/audit";
|
||||
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
||||
@@ -65,9 +66,9 @@ export const GET = withAdmin(
|
||||
|
||||
return apiOk({ metadata: json, flags });
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"[nitro-editor] Failed to parse bundle:",
|
||||
(err as Error).message,
|
||||
logger.error(
|
||||
`[nitro-editor] Failed to parse bundle: ${(err as Error).message}`,
|
||||
{ err: err as Error },
|
||||
);
|
||||
return apiError("Failed to parse bundle file", 500);
|
||||
}
|
||||
@@ -156,9 +157,8 @@ export const PUT = withAdmin(
|
||||
await writeFurniData(furniData as Record<string, unknown>);
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"[nitro-editor] FurnitureData.json update failed:",
|
||||
(err as Error).message,
|
||||
logger.warn(
|
||||
`[nitro-editor] FurnitureData.json update failed: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -173,9 +173,8 @@ export const PUT = withAdmin(
|
||||
WHERE item_name = ${rawClassname}
|
||||
`);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"[nitro-editor] items_base update failed:",
|
||||
(err as Error).message,
|
||||
logger.warn(
|
||||
`[nitro-editor] items_base update failed: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -183,9 +182,8 @@ export const PUT = withAdmin(
|
||||
try {
|
||||
await rcon.updateItems();
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"[nitro-editor] RCON updateItems failed:",
|
||||
(err as Error).message,
|
||||
logger.warn(
|
||||
`[nitro-editor] RCON updateItems failed: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -210,7 +208,7 @@ export const PUT = withAdmin(
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn("[nitro-editor] Audit log failed:", (err as Error).message);
|
||||
logger.warn(`[nitro-editor] Audit log failed: ${(err as Error).message}`);
|
||||
}
|
||||
|
||||
return apiOk({ dimensions: dims });
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@/lib/furni/bundle-file";
|
||||
import { isSafeAssetName, normalizeClassname } from "@/lib/furni/classname";
|
||||
import { localFurnitureStatus } from "@/lib/furni/local-presence";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { logAudit } from "@/lib/services/audit";
|
||||
import { getCloneList } from "@/lib/services/clone-import";
|
||||
@@ -446,9 +447,8 @@ export const POST = withAdmin(
|
||||
try {
|
||||
await appendFurniEntriesBulk(collectedFurniEntries);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"[import-furni] Bulk FurnitureData.json write failed:",
|
||||
(err as Error).message,
|
||||
logger.warn(
|
||||
`[import-furni] Bulk FurnitureData.json write failed: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -458,9 +458,8 @@ export const POST = withAdmin(
|
||||
await rcon.updateCatalog();
|
||||
await rcon.updateItems();
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"[import-furni] RCON update failed after batch:",
|
||||
(err as Error).message,
|
||||
logger.warn(
|
||||
`[import-furni] RCON update failed after batch: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
invalidateCatalogTotals();
|
||||
@@ -509,9 +508,8 @@ export const POST = withAdmin(
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"[import-furni] Localized furnidata build failed:",
|
||||
(err as Error).message,
|
||||
logger.warn(
|
||||
`[import-furni] Localized furnidata build failed: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -588,9 +586,8 @@ export const POST = withAdmin(
|
||||
await rcon.updateCatalog();
|
||||
await rcon.updateItems();
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"[import-furni] RCON update failed:",
|
||||
(err as Error).message,
|
||||
logger.warn(
|
||||
`[import-furni] RCON update failed: ${(err as Error).message}`,
|
||||
);
|
||||
result.warnings.push("RCON cache refresh failed");
|
||||
}
|
||||
@@ -640,9 +637,8 @@ export const POST = withAdmin(
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"[import-furni] Localized patch failed:",
|
||||
(err as Error).message,
|
||||
logger.warn(
|
||||
`[import-furni] Localized patch failed: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -850,9 +846,8 @@ export const PATCH = withAdmin(
|
||||
await appendFurniEntry(entry, itemType);
|
||||
}
|
||||
} catch (fdErr) {
|
||||
console.warn(
|
||||
`[regen] FurnitureData update skipped for ${item.classname}:`,
|
||||
(fdErr as Error).message,
|
||||
logger.warn(
|
||||
`[regen] FurnitureData update skipped for ${item.classname}: ${(fdErr as Error).message}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
} from "@/features/catalog/domain/organize";
|
||||
import { CATALOG_LAYOUTS, type CatalogLayout } from "@/lib/catalog-layouts";
|
||||
import { getLocalIconUrl } from "@/lib/furni/classname";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LiveCatalogPreview } from "./live-catalog-preview";
|
||||
|
||||
@@ -136,7 +137,9 @@ export function AutoCategoryDialog({
|
||||
.then((r) => r.json())
|
||||
.then((data) => setResults(data.results ?? []))
|
||||
.catch((error) =>
|
||||
console.error("[AutoCategory] furni search failed:", error),
|
||||
logger.error("[AutoCategory] furni search failed", {
|
||||
err: error as Error,
|
||||
}),
|
||||
)
|
||||
.finally(() => setSearchLoading(false));
|
||||
return () => ac.abort();
|
||||
@@ -263,7 +266,9 @@ export function AutoCategoryDialog({
|
||||
resetState();
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
console.error("[AutoCategory] create failed:", error);
|
||||
logger.error("[AutoCategory] create failed", {
|
||||
err: error as Error,
|
||||
});
|
||||
toast.error("Failed to create category");
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { ChevronRight, Home } from "lucide-react";
|
||||
import { Fragment, useEffect } from "react";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { useCatalogManager } from "./use-catalog-store";
|
||||
|
||||
export function BreadcrumbBar({
|
||||
@@ -29,7 +30,9 @@ export function BreadcrumbBar({
|
||||
}
|
||||
})
|
||||
.catch((error) =>
|
||||
console.error("[Breadcrumb] Failed to fetch ancestors:", error),
|
||||
logger.error("[Breadcrumb] Failed to fetch ancestors", {
|
||||
err: error as Error,
|
||||
}),
|
||||
);
|
||||
return () => ac.abort();
|
||||
}, [selectedPageId, dispatch, catQs]);
|
||||
|
||||
@@ -62,6 +62,7 @@ import { requestCatalogNavigation } from "@/features/catalog/client/use-catalog-
|
||||
import { useLiveCatalog } from "@/features/catalog/client/use-live-catalog";
|
||||
import { DuplicateCategoryDialog } from "@/features/catalog/components/duplicate-category-dialog";
|
||||
import type { DuplicateInput } from "@/features/catalog/domain/duplicate";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
type FlatTreeNode,
|
||||
@@ -779,7 +780,9 @@ export function SortableTree({
|
||||
});
|
||||
})
|
||||
.catch((error) =>
|
||||
console.error("[SortableTree] Failed to load children:", error),
|
||||
logger.error("[SortableTree] Failed to load children", {
|
||||
err: error as Error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { catalogueIconUrl } from "@/lib/catalog-assets";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface IconPickerProps {
|
||||
@@ -64,7 +65,9 @@ export function IconPicker({
|
||||
setIcons((prev) => (replace ? fetched : [...prev, ...fetched]));
|
||||
setOffset(nextOffset + fetched.length);
|
||||
} catch (err) {
|
||||
console.error("[IconPicker]", err);
|
||||
logger.error("[IconPicker] icon load failed", {
|
||||
err: err as Error,
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useServerAction } from "@/hooks/use-server-action";
|
||||
import { getHabboCdnIconUrl, getLocalIconUrl } from "@/lib/furni/classname";
|
||||
import { logger } from "@/lib/logger";
|
||||
|
||||
interface FurniResult {
|
||||
id: number;
|
||||
@@ -81,7 +82,11 @@ export function QuickAddFurni({
|
||||
)
|
||||
.then((r) => r.json())
|
||||
.then((data) => setResults(data.results ?? []))
|
||||
.catch((error) => console.error("[QuickAdd] Furni search failed:", error))
|
||||
.catch((error) =>
|
||||
logger.error("[QuickAdd] Furni search failed", {
|
||||
err: error as Error,
|
||||
}),
|
||||
)
|
||||
.finally(() => setLoading(false));
|
||||
return () => ac.abort();
|
||||
}, [debounced, open]);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { deleteMedia, uploadMedia } from "@/actions/admin-media";
|
||||
import { useConfirmDialog } from "@/components/admin/confirm-dialog";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { logger } from "@/lib/logger";
|
||||
import type { MediaFile } from "@/lib/media-files";
|
||||
import { loadMediaFiles, validImageUrl } from "@/lib/media-files";
|
||||
|
||||
@@ -83,7 +84,11 @@ export function AdminMediaGrid() {
|
||||
setCopied(url);
|
||||
setTimeout(() => setCopied(null), 1500);
|
||||
})
|
||||
.catch((error) => console.error("[Media] Clipboard copy failed:", error));
|
||||
.catch((error) =>
|
||||
logger.error("[Media] Clipboard copy failed", {
|
||||
err: error as Error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function remove(name: string) {
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
isValidCatalogLayout,
|
||||
LAYOUT_DESCRIPTIONS,
|
||||
} from "@/lib/catalog-layouts";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const MAX_ITEMS_PER_RUN = 500;
|
||||
@@ -245,7 +246,9 @@ export function OrganizeImportsDialog({
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[OrganizeImports] load failed:", error);
|
||||
logger.error("[OrganizeImports] load failed", {
|
||||
err: error as Error,
|
||||
});
|
||||
setLoadError(true);
|
||||
setGroups([]);
|
||||
} finally {
|
||||
@@ -358,7 +361,9 @@ export function OrganizeImportsDialog({
|
||||
);
|
||||
})
|
||||
.catch((error) =>
|
||||
console.error("[OrganizeImports] page search failed:", error),
|
||||
logger.error("[OrganizeImports] page search failed", {
|
||||
err: error as Error,
|
||||
}),
|
||||
)
|
||||
.finally(() => setDestSearchLoading(false));
|
||||
return () => ac.abort();
|
||||
@@ -761,7 +766,9 @@ export function OrganizeImportsDialog({
|
||||
}
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
console.error("[OrganizeImports] create failed:", error);
|
||||
logger.error("[OrganizeImports] create failed", {
|
||||
err: error as Error,
|
||||
});
|
||||
toast.error(t("errorFailed"));
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { useEventSource } from "@/lib/use-event-source";
|
||||
|
||||
/**
|
||||
@@ -170,7 +171,11 @@ export default function RadioPlayer() {
|
||||
.then((data) => {
|
||||
if (data !== null) setConfig(parseConfig(data));
|
||||
})
|
||||
.catch((error) => console.error("[Radio] Config fetch failed:", error));
|
||||
.catch((error) =>
|
||||
logger.error("[Radio] Config fetch failed", {
|
||||
err: error as Error,
|
||||
}),
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Keep the <audio> volume in sync with the slider.
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRef, useState, useTransition } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { logger } from "@/lib/logger";
|
||||
import type { ActionResult } from "@/lib/safe-action-shared";
|
||||
|
||||
interface UseServerActionOptions {
|
||||
@@ -97,7 +98,9 @@ export function useServerAction(options: UseServerActionOptions = {}) {
|
||||
onError?.();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[useServerAction] Unexpected error:", err);
|
||||
logger.error("[useServerAction] Unexpected error", {
|
||||
err: err as Error,
|
||||
});
|
||||
toast.error(errorMessage);
|
||||
onError?.();
|
||||
} finally {
|
||||
|
||||
+1
-8
@@ -49,14 +49,7 @@ function createDb() {
|
||||
return drizzle(instrumentDatabase(pool), {
|
||||
schema: fullSchema,
|
||||
mode: "default",
|
||||
logger:
|
||||
env.NODE_ENV === "development"
|
||||
? {
|
||||
logQuery(query, params) {
|
||||
console.log("[db]", query, params);
|
||||
},
|
||||
}
|
||||
: false,
|
||||
logger: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+31
-2
@@ -5,6 +5,9 @@ import { getOperationContext } from "./foundation/request-context";
|
||||
|
||||
type LogLevel = "debug" | "info" | "warn" | "error";
|
||||
|
||||
const isBrowser =
|
||||
typeof window !== "undefined" && typeof window.console !== "undefined";
|
||||
|
||||
const level: LogLevel =
|
||||
(env.LOG_LEVEL as LogLevel | undefined) ??
|
||||
(env.NODE_ENV === "production" ? "info" : "debug");
|
||||
@@ -32,18 +35,45 @@ export function generateRequestId(): string {
|
||||
return `${Date.now().toString(36)}-${requestIdCounter.toString(36)}`;
|
||||
}
|
||||
|
||||
/** Compatibility wrapper — existing call sites use (message, meta). */
|
||||
function formatMeta(meta: Record<string, unknown> = {}): string {
|
||||
const keys = Object.keys(meta);
|
||||
if (keys.length === 0) return "";
|
||||
return keys
|
||||
.map((k) => {
|
||||
const v = meta[k];
|
||||
return `${k}=${v instanceof Error ? v.message : String(v)}`;
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
/** Universal logger: pino on server, console in browser. Same interface everywhere. */
|
||||
export const logger = {
|
||||
debug(message: string, meta: Record<string, unknown> = {}): void {
|
||||
if (isBrowser) {
|
||||
console.debug(`[debug] ${message} ${formatMeta(meta)}`.trim());
|
||||
return;
|
||||
}
|
||||
pinoLogger.debug({ ...meta, ...getOperationContext() }, message);
|
||||
},
|
||||
info(message: string, meta: Record<string, unknown> = {}): void {
|
||||
if (isBrowser) {
|
||||
console.info(`[info] ${message} ${formatMeta(meta)}`.trim());
|
||||
return;
|
||||
}
|
||||
pinoLogger.info({ ...meta, ...getOperationContext() }, message);
|
||||
},
|
||||
warn(message: string, meta: Record<string, unknown> = {}): void {
|
||||
if (isBrowser) {
|
||||
console.warn(`[warn] ${message} ${formatMeta(meta)}`.trim());
|
||||
return;
|
||||
}
|
||||
pinoLogger.warn({ ...meta, ...getOperationContext() }, message);
|
||||
},
|
||||
error(message: string, meta: Record<string, unknown> = {}): string {
|
||||
if (isBrowser) {
|
||||
console.error(`[error] ${message} ${formatMeta(meta)}`.trim());
|
||||
return "";
|
||||
}
|
||||
const error =
|
||||
meta.err instanceof Error
|
||||
? meta.err
|
||||
@@ -57,7 +87,6 @@ export const logger = {
|
||||
: message,
|
||||
);
|
||||
const operation = getOperationContext();
|
||||
// Keep trusted correlation first so bounded error context retains it.
|
||||
const context = { ...operation, ...meta, ...operation };
|
||||
const errorId = captureCmsError("server", message, error, context);
|
||||
pinoLogger.error({ ...context, errorId }, message);
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
normalizeClassname,
|
||||
} from "@/lib/furni/classname";
|
||||
import { indexLocalFurniture } from "@/lib/furni/local-presence";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { readFurniData } from "./furni-data";
|
||||
|
||||
const CACHE_TTL = 30_000; // 30 seconds
|
||||
@@ -244,9 +245,8 @@ export async function getFurniImportStats(
|
||||
statsSnapshot = { stats, timestamp: now };
|
||||
return stats;
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"[furni-import-cache] Stats scan failed:",
|
||||
(err as Error).message,
|
||||
logger.warn(
|
||||
`[furni-import-cache] Stats scan failed: ${(err as Error).message}`,
|
||||
);
|
||||
return (
|
||||
statsSnapshot?.stats ?? {
|
||||
|
||||
@@ -534,6 +534,46 @@ export async function allocateCatalogItemId<T>(
|
||||
return await insertFn(nextId);
|
||||
}
|
||||
|
||||
/** Reserveer N sequentiële catalog item IDs uit de in-process counter, zonder
|
||||
* per-item transaction. Gebruikt door bulk insert paden. */
|
||||
export async function reserveCatalogItemIds(count: number): Promise<number[]> {
|
||||
if (count <= 0) return [];
|
||||
|
||||
const now = Date.now();
|
||||
if (
|
||||
catalogNextId === null ||
|
||||
now - catalogIdLastUsed > CATALOG_ID_REFRESH_MS
|
||||
) {
|
||||
let settle!: () => void;
|
||||
const prev = catalogIdSeedChain;
|
||||
catalogIdSeedChain = new Promise<void>((r) => {
|
||||
settle = r;
|
||||
});
|
||||
await prev.catch((error) =>
|
||||
logServerError("furni.catalog_id_chain_failed", error),
|
||||
);
|
||||
try {
|
||||
if (
|
||||
catalogNextId === null ||
|
||||
Date.now() - catalogIdLastUsed > CATALOG_ID_REFRESH_MS
|
||||
) {
|
||||
const maxIdResult = await queryRows<{
|
||||
maxId: number | bigint | null;
|
||||
}>(sql`
|
||||
SELECT MAX(id) as maxId FROM catalog_items
|
||||
`);
|
||||
catalogNextId = Number(maxIdResult[0]?.maxId ?? 0) + 1;
|
||||
}
|
||||
} finally {
|
||||
settle();
|
||||
}
|
||||
}
|
||||
catalogIdLastUsed = Date.now();
|
||||
const start = catalogNextId;
|
||||
catalogNextId += count;
|
||||
return Array.from({ length: count }, (_, i) => start + i);
|
||||
}
|
||||
|
||||
/** Test seam: drop the cached id counter between tests. */
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,7 @@ vi.mock("@/lib/services/habbo-gamedata-hotel", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/logger", () => ({
|
||||
logger: { warn: vi.fn() },
|
||||
logger: { warn: vi.fn(), debug: vi.fn() },
|
||||
}));
|
||||
|
||||
import * as cacheModule from "./habbo-furnidata-cache";
|
||||
|
||||
@@ -40,7 +40,7 @@ export async function getOfficialHabboFurnidata(): Promise<
|
||||
loading = (async () => {
|
||||
const url = habboFurnidataUrl(hotel);
|
||||
try {
|
||||
console.debug(`[habbo-furnidata] Fetching furnidata from ${url}...`);
|
||||
logger.debug(`[habbo-furnidata] Fetching furnidata from ${url}...`);
|
||||
const res = await fetch(url, {
|
||||
signal: AbortSignal.timeout(20000),
|
||||
headers: browserHeaders(),
|
||||
@@ -114,7 +114,7 @@ export async function getOfficialHabboFurnidata(): Promise<
|
||||
}
|
||||
}
|
||||
|
||||
console.debug(
|
||||
logger.debug(
|
||||
`[habbo-furnidata] Cached ${map.size} items from habbo.${hotel} (${roomTypes?.length ?? 0} room + ${wallTypes?.length ?? 0} wall)`,
|
||||
);
|
||||
cache = map;
|
||||
|
||||
@@ -106,7 +106,7 @@ async function loadBadges(): Promise<{
|
||||
|
||||
badges.sort((a, b) => a.code.localeCompare(b.code));
|
||||
|
||||
console.debug(
|
||||
logger.debug(
|
||||
`[badges] Loaded ${badges.length} badges from habbo.${hotel}`,
|
||||
);
|
||||
badgeCache = badges;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import "server-only";
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { db, WebsiteBadges } from "@/lib/db";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { getGamedataRoot } from "@/lib/services/furni-asset-dirs";
|
||||
import { getRuntimePath } from "@/lib/utils/runtime-path";
|
||||
|
||||
@@ -37,9 +39,8 @@ export async function writeBadgeToExternalTexts(
|
||||
} catch (err) {
|
||||
// File not present yet (fresh deployment) — start with an empty object.
|
||||
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
console.warn(
|
||||
"[import-badges] Failed to update ExternalTexts.json:",
|
||||
(err as Error).message,
|
||||
logger.warn(
|
||||
`[import-badges] Failed to update ExternalTexts.json: ${(err as Error).message}`,
|
||||
);
|
||||
return {
|
||||
ok: false,
|
||||
@@ -66,9 +67,8 @@ export async function writeBadgeToExternalTexts(
|
||||
await fs.writeFile(extPath, JSON.stringify(texts, null, 4), "utf-8");
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"[import-badges] Failed to update ExternalTexts.json:",
|
||||
(err as Error).message,
|
||||
logger.warn(
|
||||
`[import-badges] Failed to update ExternalTexts.json: ${(err as Error).message}`,
|
||||
);
|
||||
return {
|
||||
ok: false,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { getByparrCookies } from "@/lib/services/byparr";
|
||||
import {
|
||||
detectNitroTextureFormat,
|
||||
@@ -119,7 +120,7 @@ export async function downloadFile(
|
||||
const stat = await fs.stat(destPath);
|
||||
return { ok: true, size: stat.size };
|
||||
}
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`[import-download] Download failed ${url}: ${res.status}${deterministic ? " (deterministic, not retrying)" : ` after ${maxRetries + 1} attempts`}`,
|
||||
);
|
||||
return { ok: false, size: 0, error: `HTTP ${res.status}` };
|
||||
@@ -134,14 +135,14 @@ export async function downloadFile(
|
||||
}
|
||||
if (options?.validate === "swf" && !validateSwfBytes(buffer)) {
|
||||
if (attempt === maxRetries) {
|
||||
console.warn(`[import-download] Invalid SWF magic bytes from ${url}`);
|
||||
logger.warn(`[import-download] Invalid SWF magic bytes from ${url}`);
|
||||
return { ok: false, size: 0, error: "Response is not a SWF file" };
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (options?.validate === "png" && !validatePngBytes(buffer)) {
|
||||
if (attempt === maxRetries) {
|
||||
console.warn(`[import-download] Invalid PNG magic bytes from ${url}`);
|
||||
logger.warn(`[import-download] Invalid PNG magic bytes from ${url}`);
|
||||
return { ok: false, size: 0, error: "Response is not a PNG file" };
|
||||
}
|
||||
continue;
|
||||
@@ -159,9 +160,8 @@ export async function downloadFile(
|
||||
const stat = await fs.stat(destPath);
|
||||
return { ok: true, size: stat.size };
|
||||
}
|
||||
console.warn(
|
||||
`[import-download] Download error ${url}:`,
|
||||
(err as Error).message,
|
||||
logger.warn(
|
||||
`[import-download] Download error ${url}: ${(err as Error).message}`,
|
||||
);
|
||||
const failure = err as {
|
||||
code?: string;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import { logger } from "@/lib/logger";
|
||||
|
||||
const LOCK_STALE_MS = 60_000;
|
||||
const LOCK_ACQUIRE_TIMEOUT_MS = 30_000;
|
||||
@@ -50,7 +51,7 @@ async function waitForChain(filePath: string): Promise<void> {
|
||||
|
||||
if (elapsed > CHAIN_TIMEOUT_MS) {
|
||||
// Previous holder took too long — break the chain
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`[gamedata-lock] Breaking stale chain for ${filePath} (held for ${elapsed}ms)`,
|
||||
);
|
||||
chains.delete(filePath);
|
||||
@@ -70,7 +71,7 @@ async function waitForChain(filePath: string): Promise<void> {
|
||||
]);
|
||||
} catch {
|
||||
// Chain holder timed out or crashed — break the chain
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`[gamedata-lock] Chain timeout for ${filePath}, breaking chain`,
|
||||
);
|
||||
chains.delete(filePath);
|
||||
|
||||
Reference in new issue
Block a user