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 { PERMS } from "@/lib/permissions";
|
||||||
import { logAudit } from "@/lib/services/audit";
|
import { logAudit } from "@/lib/services/audit";
|
||||||
import { withCatalogExport } from "@/lib/services/catalog-git-queue";
|
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 { logStaffActivity } from "@/lib/services/staff-activity";
|
||||||
import { translateItemsSchema } from "@/lib/validators/catalog";
|
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. */
|
/** Bulk create with one RCON refresh at the end. */
|
||||||
export async function bulkCreateCatalogItems({
|
export async function bulkCreateCatalogItems({
|
||||||
pageId,
|
pageId,
|
||||||
@@ -155,67 +290,17 @@ export async function bulkCreateCatalogItems({
|
|||||||
return { ok: false as const, error: "Max 500 items per bulk import" };
|
return { ok: false as const, error: "Max 500 items per bulk import" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseIds = [...new Set(rows.map((r) => r.baseId))];
|
const { created, failed } = await bulkInsertCatalogItems({
|
||||||
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({
|
|
||||||
pageId,
|
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,
|
staffId: staff.id,
|
||||||
action: "catalog_items_bulk_create",
|
rows: rows.map((r) => ({
|
||||||
description: `Bulk imported ${created} catalog item(s) on page #${pageId}`,
|
baseId: r.baseId,
|
||||||
targetType: "catalog_page",
|
credits: r.credits,
|
||||||
targetId: pageId,
|
points: r.points,
|
||||||
|
pointsType: r.pointsType,
|
||||||
|
amount: 1,
|
||||||
|
})),
|
||||||
});
|
});
|
||||||
revalidatePath("/admin/catalog");
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ok: true as const, data: { created, failed } };
|
return { ok: true as const, data: { created, failed } };
|
||||||
});
|
});
|
||||||
@@ -504,6 +589,19 @@ export async function translateCatalogItems(input: {
|
|||||||
"@/lib/services/furni-data-i18n"
|
"@/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 namesUpdated = 0;
|
||||||
let descriptionsUpdated = 0;
|
let descriptionsUpdated = 0;
|
||||||
const furniPatches: Array<{
|
const furniPatches: Array<{
|
||||||
@@ -519,19 +617,14 @@ export async function translateCatalogItems(input: {
|
|||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
const nameUpdates: Array<{ id: number; nextName: string }> = [];
|
||||||
|
const catalogNameUpdates: Array<{
|
||||||
|
idStr: string;
|
||||||
|
nextName: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const [base] = await db
|
const base = baseById.get(item.id);
|
||||||
.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);
|
|
||||||
if (!base) continue;
|
if (!base) continue;
|
||||||
|
|
||||||
const nextName = item.publicName?.trim() ?? "";
|
const nextName = item.publicName?.trim() ?? "";
|
||||||
@@ -540,11 +633,43 @@ export async function translateCatalogItems(input: {
|
|||||||
nextName !== "" && nextName !== (base.publicName ?? "");
|
nextName !== "" && nextName !== (base.publicName ?? "");
|
||||||
|
|
||||||
if (nameChanged) {
|
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
|
await db
|
||||||
.update(ItemsBase)
|
.update(ItemsBase)
|
||||||
.set({ publicName: nextName })
|
.set({ publicName: nextName })
|
||||||
.where(eq(ItemsBase.id, base.id));
|
.where(eq(ItemsBase.id, id));
|
||||||
const idStr = String(base.id);
|
}
|
||||||
|
|
||||||
|
// Batch CatalogItems name updates: one SELECT + UPDATE per idStr.
|
||||||
|
for (const { idStr, nextName } of catalogNameUpdates) {
|
||||||
const related = await db
|
const related = await db
|
||||||
.select({
|
.select({
|
||||||
id: CatalogItems.id,
|
id: CatalogItems.id,
|
||||||
@@ -567,26 +692,6 @@ export async function translateCatalogItems(input: {
|
|||||||
.where(eq(CatalogItems.id, row.id));
|
.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 =
|
const furniResult =
|
||||||
|
|||||||
+13
-45
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
import { inArray, sql } from "drizzle-orm";
|
import { inArray, sql } from "drizzle-orm";
|
||||||
import { revalidatePath } from "next/cache";
|
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 { catalogFailure } from "@/features/catalog/server/errors";
|
||||||
import {
|
import {
|
||||||
createPageCommand,
|
createPageCommand,
|
||||||
@@ -279,52 +282,17 @@ export async function createAutoCategory(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rows = input.offerRows ?? [];
|
const rows = input.offerRows ?? [];
|
||||||
const baseIds = [...new Set(rows.map((r) => r.baseId))];
|
const { created } = await bulkInsertCatalogItems({
|
||||||
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({
|
|
||||||
pageId,
|
pageId,
|
||||||
itemIds: String(row.baseId),
|
staffId: staff.id,
|
||||||
catalogName: base.publicName || base.itemName || String(row.baseId),
|
rows: rows.map((r) => ({
|
||||||
costCredits: row.credits ?? 0,
|
baseId: r.baseId,
|
||||||
costPoints: row.points ?? 0,
|
credits: r.credits,
|
||||||
pointsType: row.pointsType ?? 0,
|
points: r.points,
|
||||||
amount: row.amount ?? 1,
|
pointsType: r.pointsType,
|
||||||
limitedSells: 0,
|
amount: r.amount,
|
||||||
limitedStack: 0,
|
})),
|
||||||
orderNumber: nextOrder++,
|
|
||||||
offerId: -1,
|
|
||||||
songId: 0,
|
|
||||||
haveOffer: "1",
|
|
||||||
clubOnly: "0",
|
|
||||||
extradata: "",
|
|
||||||
});
|
});
|
||||||
created++;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`[createAutoCategory] failed to insert item #${row.baseId}:`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await logStaffActivity({
|
await logStaffActivity({
|
||||||
staffId: staff.id,
|
staffId: staff.id,
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { useServerAction } from "@/hooks/use-server-action";
|
import { useServerAction } from "@/hooks/use-server-action";
|
||||||
import { adminFetch } from "@/lib/admin-fetch";
|
import { adminFetch } from "@/lib/admin-fetch";
|
||||||
import { invalidateSongPickerCache } from "@/lib/client-cache/song-picker-cache";
|
import { invalidateSongPickerCache } from "@/lib/client-cache/song-picker-cache";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
import { parseTraxChannels } from "@/lib/trax-format";
|
import { parseTraxChannels } from "@/lib/trax-format";
|
||||||
import { TraxPlayer } from "./trax-player";
|
import { TraxPlayer } from "./trax-player";
|
||||||
|
|
||||||
@@ -455,7 +456,9 @@ function UploadDialog({
|
|||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
onUploaded();
|
onUploaded();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
logger.error("Sound upload failed", {
|
||||||
|
err: err as Error,
|
||||||
|
});
|
||||||
toast.error("Network error during upload.");
|
toast.error("Network error during upload.");
|
||||||
} finally {
|
} finally {
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { Loader2, Play, Square } from "lucide-react";
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
import { TRAX_TICK_SECONDS, type TraxChannel } from "@/lib/trax-format";
|
import { TRAX_TICK_SECONDS, type TraxChannel } from "@/lib/trax-format";
|
||||||
|
|
||||||
const TICK_SECONDS = TRAX_TICK_SECONDS;
|
const TICK_SECONDS = TRAX_TICK_SECONDS;
|
||||||
@@ -127,7 +128,9 @@ export function TraxPlayer({
|
|||||||
buffers.set(id, buf);
|
buffers.set(id, buf);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
failed++;
|
failed++;
|
||||||
console.warn(`[trax] failed to load sample ${id}:`, err);
|
logger.warn(`[trax] failed to load sample ${id}`, {
|
||||||
|
err: err as Error,
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
done++;
|
done++;
|
||||||
setLoaded(done);
|
setLoaded(done);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
bundleExistsInDir,
|
bundleExistsInDir,
|
||||||
bundleWritePathInDir,
|
bundleWritePathInDir,
|
||||||
} from "@/lib/furni/bundle-file";
|
} from "@/lib/furni/bundle-file";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
||||||
import {
|
import {
|
||||||
@@ -315,9 +316,8 @@ export const POST = withAdmin(
|
|||||||
try {
|
try {
|
||||||
languages = await buildLocalizedFurniDataFiles();
|
languages = await buildLocalizedFurniDataFiles();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(
|
logger.warn(
|
||||||
"[batch-regen] Localized furnidata build failed:",
|
`[batch-regen] Localized furnidata build failed: ${(err as Error).message}`,
|
||||||
(err as Error).message,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { apiError } from "@/lib/api";
|
|||||||
import { withAdmin } from "@/lib/api-handler";
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
import { getRequestId } from "@/lib/foundation/request-context";
|
import { getRequestId } from "@/lib/foundation/request-context";
|
||||||
import type { ImportJob } from "@/lib/furni/import-job";
|
import type { ImportJob } from "@/lib/furni/import-job";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { logAudit } from "@/lib/services/audit";
|
import { logAudit } from "@/lib/services/audit";
|
||||||
import { getSource } from "@/lib/services/clone-sources";
|
import { getSource } from "@/lib/services/clone-sources";
|
||||||
@@ -112,10 +113,9 @@ export const POST = withAdmin(
|
|||||||
await store.save(job);
|
await store.save(job);
|
||||||
return job;
|
return job;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
logger.warn("[import-furni] Could not create durable batch job", {
|
||||||
"[import-furni] Could not create durable batch job",
|
err: error as Error,
|
||||||
error,
|
});
|
||||||
);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@@ -156,7 +156,9 @@ export const POST = withAdmin(
|
|||||||
try {
|
try {
|
||||||
await store.save(mirrorJob);
|
await store.save(mirrorJob);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[import-furni] Checkpoint write failed", error);
|
logger.warn("[import-furni] Checkpoint write failed", {
|
||||||
|
err: error as Error,
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
saveQueued = false;
|
saveQueued = false;
|
||||||
}
|
}
|
||||||
@@ -177,7 +179,9 @@ export const POST = withAdmin(
|
|||||||
try {
|
try {
|
||||||
await store.save(mirrorJob);
|
await store.save(mirrorJob);
|
||||||
} catch (error) {
|
} 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) {
|
} catch (err) {
|
||||||
console.warn(
|
logger.warn(
|
||||||
"[import-furni] RCON update failed after batch:",
|
`[import-furni] RCON update failed after batch: ${(err as Error).message}`,
|
||||||
(err as Error).message,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -470,15 +473,13 @@ export const POST = withAdmin(
|
|||||||
try {
|
try {
|
||||||
languages = await buildLocalizedFurniDataFiles();
|
languages = await buildLocalizedFurniDataFiles();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(
|
logger.warn(
|
||||||
"[import-furni] Localized furnidata build failed:",
|
`[import-furni] Localized furnidata build failed: ${(err as Error).message}`,
|
||||||
(err as Error).message,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(
|
logger.warn(
|
||||||
"[import-furni] Post-import reconcile/ownership failed:",
|
`[import-furni] Post-import reconcile/ownership failed: ${(err as Error).message}`,
|
||||||
(err as Error).message,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { apiError, apiOk } from "@/lib/api";
|
|||||||
import { withAdmin } from "@/lib/api-handler";
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
import { db, ItemsBase, queryRows } from "@/lib/db";
|
import { db, ItemsBase, queryRows } from "@/lib/db";
|
||||||
import { resolveBundleInDir } from "@/lib/furni/bundle-file";
|
import { resolveBundleInDir } from "@/lib/furni/bundle-file";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { logAudit } from "@/lib/services/audit";
|
import { logAudit } from "@/lib/services/audit";
|
||||||
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
||||||
@@ -65,9 +66,9 @@ export const GET = withAdmin(
|
|||||||
|
|
||||||
return apiOk({ metadata: json, flags });
|
return apiOk({ metadata: json, flags });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(
|
logger.error(
|
||||||
"[nitro-editor] Failed to parse bundle:",
|
`[nitro-editor] Failed to parse bundle: ${(err as Error).message}`,
|
||||||
(err as Error).message,
|
{ err: err as Error },
|
||||||
);
|
);
|
||||||
return apiError("Failed to parse bundle file", 500);
|
return apiError("Failed to parse bundle file", 500);
|
||||||
}
|
}
|
||||||
@@ -156,9 +157,8 @@ export const PUT = withAdmin(
|
|||||||
await writeFurniData(furniData as Record<string, unknown>);
|
await writeFurniData(furniData as Record<string, unknown>);
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(
|
logger.warn(
|
||||||
"[nitro-editor] FurnitureData.json update failed:",
|
`[nitro-editor] FurnitureData.json update failed: ${(err as Error).message}`,
|
||||||
(err as Error).message,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,9 +173,8 @@ export const PUT = withAdmin(
|
|||||||
WHERE item_name = ${rawClassname}
|
WHERE item_name = ${rawClassname}
|
||||||
`);
|
`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(
|
logger.warn(
|
||||||
"[nitro-editor] items_base update failed:",
|
`[nitro-editor] items_base update failed: ${(err as Error).message}`,
|
||||||
(err as Error).message,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,9 +182,8 @@ export const PUT = withAdmin(
|
|||||||
try {
|
try {
|
||||||
await rcon.updateItems();
|
await rcon.updateItems();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(
|
logger.warn(
|
||||||
"[nitro-editor] RCON updateItems failed:",
|
`[nitro-editor] RCON updateItems failed: ${(err as Error).message}`,
|
||||||
(err as Error).message,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +208,7 @@ export const PUT = withAdmin(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} 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 });
|
return apiOk({ dimensions: dims });
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from "@/lib/furni/bundle-file";
|
} from "@/lib/furni/bundle-file";
|
||||||
import { isSafeAssetName, normalizeClassname } from "@/lib/furni/classname";
|
import { isSafeAssetName, normalizeClassname } from "@/lib/furni/classname";
|
||||||
import { localFurnitureStatus } from "@/lib/furni/local-presence";
|
import { localFurnitureStatus } from "@/lib/furni/local-presence";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { logAudit } from "@/lib/services/audit";
|
import { logAudit } from "@/lib/services/audit";
|
||||||
import { getCloneList } from "@/lib/services/clone-import";
|
import { getCloneList } from "@/lib/services/clone-import";
|
||||||
@@ -446,9 +447,8 @@ export const POST = withAdmin(
|
|||||||
try {
|
try {
|
||||||
await appendFurniEntriesBulk(collectedFurniEntries);
|
await appendFurniEntriesBulk(collectedFurniEntries);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(
|
logger.warn(
|
||||||
"[import-furni] Bulk FurnitureData.json write failed:",
|
`[import-furni] Bulk FurnitureData.json write failed: ${(err as Error).message}`,
|
||||||
(err as Error).message,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -458,9 +458,8 @@ export const POST = withAdmin(
|
|||||||
await rcon.updateCatalog();
|
await rcon.updateCatalog();
|
||||||
await rcon.updateItems();
|
await rcon.updateItems();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(
|
logger.warn(
|
||||||
"[import-furni] RCON update failed after batch:",
|
`[import-furni] RCON update failed after batch: ${(err as Error).message}`,
|
||||||
(err as Error).message,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
invalidateCatalogTotals();
|
invalidateCatalogTotals();
|
||||||
@@ -509,9 +508,8 @@ export const POST = withAdmin(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(
|
logger.warn(
|
||||||
"[import-furni] Localized furnidata build failed:",
|
`[import-furni] Localized furnidata build failed: ${(err as Error).message}`,
|
||||||
(err as Error).message,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -588,9 +586,8 @@ export const POST = withAdmin(
|
|||||||
await rcon.updateCatalog();
|
await rcon.updateCatalog();
|
||||||
await rcon.updateItems();
|
await rcon.updateItems();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(
|
logger.warn(
|
||||||
"[import-furni] RCON update failed:",
|
`[import-furni] RCON update failed: ${(err as Error).message}`,
|
||||||
(err as Error).message,
|
|
||||||
);
|
);
|
||||||
result.warnings.push("RCON cache refresh failed");
|
result.warnings.push("RCON cache refresh failed");
|
||||||
}
|
}
|
||||||
@@ -640,9 +637,8 @@ export const POST = withAdmin(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(
|
logger.warn(
|
||||||
"[import-furni] Localized patch failed:",
|
`[import-furni] Localized patch failed: ${(err as Error).message}`,
|
||||||
(err as Error).message,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -850,9 +846,8 @@ export const PATCH = withAdmin(
|
|||||||
await appendFurniEntry(entry, itemType);
|
await appendFurniEntry(entry, itemType);
|
||||||
}
|
}
|
||||||
} catch (fdErr) {
|
} catch (fdErr) {
|
||||||
console.warn(
|
logger.warn(
|
||||||
`[regen] FurnitureData update skipped for ${item.classname}:`,
|
`[regen] FurnitureData update skipped for ${item.classname}: ${(fdErr as Error).message}`,
|
||||||
(fdErr as Error).message,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
} from "@/features/catalog/domain/organize";
|
} from "@/features/catalog/domain/organize";
|
||||||
import { CATALOG_LAYOUTS, type CatalogLayout } from "@/lib/catalog-layouts";
|
import { CATALOG_LAYOUTS, type CatalogLayout } from "@/lib/catalog-layouts";
|
||||||
import { getLocalIconUrl } from "@/lib/furni/classname";
|
import { getLocalIconUrl } from "@/lib/furni/classname";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { LiveCatalogPreview } from "./live-catalog-preview";
|
import { LiveCatalogPreview } from "./live-catalog-preview";
|
||||||
|
|
||||||
@@ -136,7 +137,9 @@ export function AutoCategoryDialog({
|
|||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((data) => setResults(data.results ?? []))
|
.then((data) => setResults(data.results ?? []))
|
||||||
.catch((error) =>
|
.catch((error) =>
|
||||||
console.error("[AutoCategory] furni search failed:", error),
|
logger.error("[AutoCategory] furni search failed", {
|
||||||
|
err: error as Error,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.finally(() => setSearchLoading(false));
|
.finally(() => setSearchLoading(false));
|
||||||
return () => ac.abort();
|
return () => ac.abort();
|
||||||
@@ -263,7 +266,9 @@ export function AutoCategoryDialog({
|
|||||||
resetState();
|
resetState();
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[AutoCategory] create failed:", error);
|
logger.error("[AutoCategory] create failed", {
|
||||||
|
err: error as Error,
|
||||||
|
});
|
||||||
toast.error("Failed to create category");
|
toast.error("Failed to create category");
|
||||||
} finally {
|
} finally {
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { ChevronRight, Home } from "lucide-react";
|
import { ChevronRight, Home } from "lucide-react";
|
||||||
import { Fragment, useEffect } from "react";
|
import { Fragment, useEffect } from "react";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
import { useCatalogManager } from "./use-catalog-store";
|
import { useCatalogManager } from "./use-catalog-store";
|
||||||
|
|
||||||
export function BreadcrumbBar({
|
export function BreadcrumbBar({
|
||||||
@@ -29,7 +30,9 @@ export function BreadcrumbBar({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) =>
|
.catch((error) =>
|
||||||
console.error("[Breadcrumb] Failed to fetch ancestors:", error),
|
logger.error("[Breadcrumb] Failed to fetch ancestors", {
|
||||||
|
err: error as Error,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
return () => ac.abort();
|
return () => ac.abort();
|
||||||
}, [selectedPageId, dispatch, catQs]);
|
}, [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 { useLiveCatalog } from "@/features/catalog/client/use-live-catalog";
|
||||||
import { DuplicateCategoryDialog } from "@/features/catalog/components/duplicate-category-dialog";
|
import { DuplicateCategoryDialog } from "@/features/catalog/components/duplicate-category-dialog";
|
||||||
import type { DuplicateInput } from "@/features/catalog/domain/duplicate";
|
import type { DuplicateInput } from "@/features/catalog/domain/duplicate";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
type FlatTreeNode,
|
type FlatTreeNode,
|
||||||
@@ -779,7 +780,9 @@ export function SortableTree({
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((error) =>
|
.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";
|
} from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { catalogueIconUrl } from "@/lib/catalog-assets";
|
import { catalogueIconUrl } from "@/lib/catalog-assets";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface IconPickerProps {
|
interface IconPickerProps {
|
||||||
@@ -64,7 +65,9 @@ export function IconPicker({
|
|||||||
setIcons((prev) => (replace ? fetched : [...prev, ...fetched]));
|
setIcons((prev) => (replace ? fetched : [...prev, ...fetched]));
|
||||||
setOffset(nextOffset + fetched.length);
|
setOffset(nextOffset + fetched.length);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[IconPicker]", err);
|
logger.error("[IconPicker] icon load failed", {
|
||||||
|
err: err as Error,
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { useServerAction } from "@/hooks/use-server-action";
|
import { useServerAction } from "@/hooks/use-server-action";
|
||||||
import { getHabboCdnIconUrl, getLocalIconUrl } from "@/lib/furni/classname";
|
import { getHabboCdnIconUrl, getLocalIconUrl } from "@/lib/furni/classname";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
|
|
||||||
interface FurniResult {
|
interface FurniResult {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -81,7 +82,11 @@ export function QuickAddFurni({
|
|||||||
)
|
)
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((data) => setResults(data.results ?? []))
|
.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));
|
.finally(() => setLoading(false));
|
||||||
return () => ac.abort();
|
return () => ac.abort();
|
||||||
}, [debounced, open]);
|
}, [debounced, open]);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|||||||
import { deleteMedia, uploadMedia } from "@/actions/admin-media";
|
import { deleteMedia, uploadMedia } from "@/actions/admin-media";
|
||||||
import { useConfirmDialog } from "@/components/admin/confirm-dialog";
|
import { useConfirmDialog } from "@/components/admin/confirm-dialog";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
import type { MediaFile } from "@/lib/media-files";
|
import type { MediaFile } from "@/lib/media-files";
|
||||||
import { loadMediaFiles, validImageUrl } from "@/lib/media-files";
|
import { loadMediaFiles, validImageUrl } from "@/lib/media-files";
|
||||||
|
|
||||||
@@ -83,7 +84,11 @@ export function AdminMediaGrid() {
|
|||||||
setCopied(url);
|
setCopied(url);
|
||||||
setTimeout(() => setCopied(null), 1500);
|
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) {
|
async function remove(name: string) {
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ import {
|
|||||||
isValidCatalogLayout,
|
isValidCatalogLayout,
|
||||||
LAYOUT_DESCRIPTIONS,
|
LAYOUT_DESCRIPTIONS,
|
||||||
} from "@/lib/catalog-layouts";
|
} from "@/lib/catalog-layouts";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const MAX_ITEMS_PER_RUN = 500;
|
const MAX_ITEMS_PER_RUN = 500;
|
||||||
@@ -245,7 +246,9 @@ export function OrganizeImportsDialog({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[OrganizeImports] load failed:", error);
|
logger.error("[OrganizeImports] load failed", {
|
||||||
|
err: error as Error,
|
||||||
|
});
|
||||||
setLoadError(true);
|
setLoadError(true);
|
||||||
setGroups([]);
|
setGroups([]);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -358,7 +361,9 @@ export function OrganizeImportsDialog({
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch((error) =>
|
.catch((error) =>
|
||||||
console.error("[OrganizeImports] page search failed:", error),
|
logger.error("[OrganizeImports] page search failed", {
|
||||||
|
err: error as Error,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.finally(() => setDestSearchLoading(false));
|
.finally(() => setDestSearchLoading(false));
|
||||||
return () => ac.abort();
|
return () => ac.abort();
|
||||||
@@ -761,7 +766,9 @@ export function OrganizeImportsDialog({
|
|||||||
}
|
}
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[OrganizeImports] create failed:", error);
|
logger.error("[OrganizeImports] create failed", {
|
||||||
|
err: error as Error,
|
||||||
|
});
|
||||||
toast.error(t("errorFailed"));
|
toast.error(t("errorFailed"));
|
||||||
} finally {
|
} finally {
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
import { useEventSource } from "@/lib/use-event-source";
|
import { useEventSource } from "@/lib/use-event-source";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -170,7 +171,11 @@ export default function RadioPlayer() {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data !== null) setConfig(parseConfig(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.
|
// Keep the <audio> volume in sync with the slider.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRef, useState, useTransition } from "react";
|
import { useRef, useState, useTransition } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
import type { ActionResult } from "@/lib/safe-action-shared";
|
import type { ActionResult } from "@/lib/safe-action-shared";
|
||||||
|
|
||||||
interface UseServerActionOptions {
|
interface UseServerActionOptions {
|
||||||
@@ -97,7 +98,9 @@ export function useServerAction(options: UseServerActionOptions = {}) {
|
|||||||
onError?.();
|
onError?.();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[useServerAction] Unexpected error:", err);
|
logger.error("[useServerAction] Unexpected error", {
|
||||||
|
err: err as Error,
|
||||||
|
});
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
onError?.();
|
onError?.();
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
+1
-8
@@ -49,14 +49,7 @@ function createDb() {
|
|||||||
return drizzle(instrumentDatabase(pool), {
|
return drizzle(instrumentDatabase(pool), {
|
||||||
schema: fullSchema,
|
schema: fullSchema,
|
||||||
mode: "default",
|
mode: "default",
|
||||||
logger:
|
logger: false,
|
||||||
env.NODE_ENV === "development"
|
|
||||||
? {
|
|
||||||
logQuery(query, params) {
|
|
||||||
console.log("[db]", query, params);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: false,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+31
-2
@@ -5,6 +5,9 @@ import { getOperationContext } from "./foundation/request-context";
|
|||||||
|
|
||||||
type LogLevel = "debug" | "info" | "warn" | "error";
|
type LogLevel = "debug" | "info" | "warn" | "error";
|
||||||
|
|
||||||
|
const isBrowser =
|
||||||
|
typeof window !== "undefined" && typeof window.console !== "undefined";
|
||||||
|
|
||||||
const level: LogLevel =
|
const level: LogLevel =
|
||||||
(env.LOG_LEVEL as LogLevel | undefined) ??
|
(env.LOG_LEVEL as LogLevel | undefined) ??
|
||||||
(env.NODE_ENV === "production" ? "info" : "debug");
|
(env.NODE_ENV === "production" ? "info" : "debug");
|
||||||
@@ -32,18 +35,45 @@ export function generateRequestId(): string {
|
|||||||
return `${Date.now().toString(36)}-${requestIdCounter.toString(36)}`;
|
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 = {
|
export const logger = {
|
||||||
debug(message: string, meta: Record<string, unknown> = {}): void {
|
debug(message: string, meta: Record<string, unknown> = {}): void {
|
||||||
|
if (isBrowser) {
|
||||||
|
console.debug(`[debug] ${message} ${formatMeta(meta)}`.trim());
|
||||||
|
return;
|
||||||
|
}
|
||||||
pinoLogger.debug({ ...meta, ...getOperationContext() }, message);
|
pinoLogger.debug({ ...meta, ...getOperationContext() }, message);
|
||||||
},
|
},
|
||||||
info(message: string, meta: Record<string, unknown> = {}): void {
|
info(message: string, meta: Record<string, unknown> = {}): void {
|
||||||
|
if (isBrowser) {
|
||||||
|
console.info(`[info] ${message} ${formatMeta(meta)}`.trim());
|
||||||
|
return;
|
||||||
|
}
|
||||||
pinoLogger.info({ ...meta, ...getOperationContext() }, message);
|
pinoLogger.info({ ...meta, ...getOperationContext() }, message);
|
||||||
},
|
},
|
||||||
warn(message: string, meta: Record<string, unknown> = {}): void {
|
warn(message: string, meta: Record<string, unknown> = {}): void {
|
||||||
|
if (isBrowser) {
|
||||||
|
console.warn(`[warn] ${message} ${formatMeta(meta)}`.trim());
|
||||||
|
return;
|
||||||
|
}
|
||||||
pinoLogger.warn({ ...meta, ...getOperationContext() }, message);
|
pinoLogger.warn({ ...meta, ...getOperationContext() }, message);
|
||||||
},
|
},
|
||||||
error(message: string, meta: Record<string, unknown> = {}): string {
|
error(message: string, meta: Record<string, unknown> = {}): string {
|
||||||
|
if (isBrowser) {
|
||||||
|
console.error(`[error] ${message} ${formatMeta(meta)}`.trim());
|
||||||
|
return "";
|
||||||
|
}
|
||||||
const error =
|
const error =
|
||||||
meta.err instanceof Error
|
meta.err instanceof Error
|
||||||
? meta.err
|
? meta.err
|
||||||
@@ -57,7 +87,6 @@ export const logger = {
|
|||||||
: message,
|
: message,
|
||||||
);
|
);
|
||||||
const operation = getOperationContext();
|
const operation = getOperationContext();
|
||||||
// Keep trusted correlation first so bounded error context retains it.
|
|
||||||
const context = { ...operation, ...meta, ...operation };
|
const context = { ...operation, ...meta, ...operation };
|
||||||
const errorId = captureCmsError("server", message, error, context);
|
const errorId = captureCmsError("server", message, error, context);
|
||||||
pinoLogger.error({ ...context, errorId }, message);
|
pinoLogger.error({ ...context, errorId }, message);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
normalizeClassname,
|
normalizeClassname,
|
||||||
} from "@/lib/furni/classname";
|
} from "@/lib/furni/classname";
|
||||||
import { indexLocalFurniture } from "@/lib/furni/local-presence";
|
import { indexLocalFurniture } from "@/lib/furni/local-presence";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
import { readFurniData } from "./furni-data";
|
import { readFurniData } from "./furni-data";
|
||||||
|
|
||||||
const CACHE_TTL = 30_000; // 30 seconds
|
const CACHE_TTL = 30_000; // 30 seconds
|
||||||
@@ -244,9 +245,8 @@ export async function getFurniImportStats(
|
|||||||
statsSnapshot = { stats, timestamp: now };
|
statsSnapshot = { stats, timestamp: now };
|
||||||
return stats;
|
return stats;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(
|
logger.warn(
|
||||||
"[furni-import-cache] Stats scan failed:",
|
`[furni-import-cache] Stats scan failed: ${(err as Error).message}`,
|
||||||
(err as Error).message,
|
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
statsSnapshot?.stats ?? {
|
statsSnapshot?.stats ?? {
|
||||||
|
|||||||
@@ -534,6 +534,46 @@ export async function allocateCatalogItemId<T>(
|
|||||||
return await insertFn(nextId);
|
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. */
|
/** Test seam: drop the cached id counter between tests. */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ vi.mock("@/lib/services/habbo-gamedata-hotel", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/lib/logger", () => ({
|
vi.mock("@/lib/logger", () => ({
|
||||||
logger: { warn: vi.fn() },
|
logger: { warn: vi.fn(), debug: vi.fn() },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import * as cacheModule from "./habbo-furnidata-cache";
|
import * as cacheModule from "./habbo-furnidata-cache";
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export async function getOfficialHabboFurnidata(): Promise<
|
|||||||
loading = (async () => {
|
loading = (async () => {
|
||||||
const url = habboFurnidataUrl(hotel);
|
const url = habboFurnidataUrl(hotel);
|
||||||
try {
|
try {
|
||||||
console.debug(`[habbo-furnidata] Fetching furnidata from ${url}...`);
|
logger.debug(`[habbo-furnidata] Fetching furnidata from ${url}...`);
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
signal: AbortSignal.timeout(20000),
|
signal: AbortSignal.timeout(20000),
|
||||||
headers: browserHeaders(),
|
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)`,
|
`[habbo-furnidata] Cached ${map.size} items from habbo.${hotel} (${roomTypes?.length ?? 0} room + ${wallTypes?.length ?? 0} wall)`,
|
||||||
);
|
);
|
||||||
cache = map;
|
cache = map;
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ async function loadBadges(): Promise<{
|
|||||||
|
|
||||||
badges.sort((a, b) => a.code.localeCompare(b.code));
|
badges.sort((a, b) => a.code.localeCompare(b.code));
|
||||||
|
|
||||||
console.debug(
|
logger.debug(
|
||||||
`[badges] Loaded ${badges.length} badges from habbo.${hotel}`,
|
`[badges] Loaded ${badges.length} badges from habbo.${hotel}`,
|
||||||
);
|
);
|
||||||
badgeCache = badges;
|
badgeCache = badges;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import "server-only";
|
||||||
import { promises as fs } from "node:fs";
|
import { promises as fs } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { db, WebsiteBadges } from "@/lib/db";
|
import { db, WebsiteBadges } from "@/lib/db";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
import { getGamedataRoot } from "@/lib/services/furni-asset-dirs";
|
import { getGamedataRoot } from "@/lib/services/furni-asset-dirs";
|
||||||
import { getRuntimePath } from "@/lib/utils/runtime-path";
|
import { getRuntimePath } from "@/lib/utils/runtime-path";
|
||||||
|
|
||||||
@@ -37,9 +39,8 @@ export async function writeBadgeToExternalTexts(
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// File not present yet (fresh deployment) — start with an empty object.
|
// File not present yet (fresh deployment) — start with an empty object.
|
||||||
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||||
console.warn(
|
logger.warn(
|
||||||
"[import-badges] Failed to update ExternalTexts.json:",
|
`[import-badges] Failed to update ExternalTexts.json: ${(err as Error).message}`,
|
||||||
(err as Error).message,
|
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -66,9 +67,8 @@ export async function writeBadgeToExternalTexts(
|
|||||||
await fs.writeFile(extPath, JSON.stringify(texts, null, 4), "utf-8");
|
await fs.writeFile(extPath, JSON.stringify(texts, null, 4), "utf-8");
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(
|
logger.warn(
|
||||||
"[import-badges] Failed to update ExternalTexts.json:",
|
`[import-badges] Failed to update ExternalTexts.json: ${(err as Error).message}`,
|
||||||
(err as Error).message,
|
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { promises as fs } from "node:fs";
|
import { promises as fs } from "node:fs";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
import { getByparrCookies } from "@/lib/services/byparr";
|
import { getByparrCookies } from "@/lib/services/byparr";
|
||||||
import {
|
import {
|
||||||
detectNitroTextureFormat,
|
detectNitroTextureFormat,
|
||||||
@@ -119,7 +120,7 @@ export async function downloadFile(
|
|||||||
const stat = await fs.stat(destPath);
|
const stat = await fs.stat(destPath);
|
||||||
return { ok: true, size: stat.size };
|
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`}`,
|
`[import-download] Download failed ${url}: ${res.status}${deterministic ? " (deterministic, not retrying)" : ` after ${maxRetries + 1} attempts`}`,
|
||||||
);
|
);
|
||||||
return { ok: false, size: 0, error: `HTTP ${res.status}` };
|
return { ok: false, size: 0, error: `HTTP ${res.status}` };
|
||||||
@@ -134,14 +135,14 @@ export async function downloadFile(
|
|||||||
}
|
}
|
||||||
if (options?.validate === "swf" && !validateSwfBytes(buffer)) {
|
if (options?.validate === "swf" && !validateSwfBytes(buffer)) {
|
||||||
if (attempt === maxRetries) {
|
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" };
|
return { ok: false, size: 0, error: "Response is not a SWF file" };
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (options?.validate === "png" && !validatePngBytes(buffer)) {
|
if (options?.validate === "png" && !validatePngBytes(buffer)) {
|
||||||
if (attempt === maxRetries) {
|
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" };
|
return { ok: false, size: 0, error: "Response is not a PNG file" };
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -159,9 +160,8 @@ export async function downloadFile(
|
|||||||
const stat = await fs.stat(destPath);
|
const stat = await fs.stat(destPath);
|
||||||
return { ok: true, size: stat.size };
|
return { ok: true, size: stat.size };
|
||||||
}
|
}
|
||||||
console.warn(
|
logger.warn(
|
||||||
`[import-download] Download error ${url}:`,
|
`[import-download] Download error ${url}: ${(err as Error).message}`,
|
||||||
(err as Error).message,
|
|
||||||
);
|
);
|
||||||
const failure = err as {
|
const failure = err as {
|
||||||
code?: string;
|
code?: string;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { promises as fs } from "node:fs";
|
import { promises as fs } from "node:fs";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
|
|
||||||
const LOCK_STALE_MS = 60_000;
|
const LOCK_STALE_MS = 60_000;
|
||||||
const LOCK_ACQUIRE_TIMEOUT_MS = 30_000;
|
const LOCK_ACQUIRE_TIMEOUT_MS = 30_000;
|
||||||
@@ -50,7 +51,7 @@ async function waitForChain(filePath: string): Promise<void> {
|
|||||||
|
|
||||||
if (elapsed > CHAIN_TIMEOUT_MS) {
|
if (elapsed > CHAIN_TIMEOUT_MS) {
|
||||||
// Previous holder took too long — break the chain
|
// Previous holder took too long — break the chain
|
||||||
console.warn(
|
logger.warn(
|
||||||
`[gamedata-lock] Breaking stale chain for ${filePath} (held for ${elapsed}ms)`,
|
`[gamedata-lock] Breaking stale chain for ${filePath} (held for ${elapsed}ms)`,
|
||||||
);
|
);
|
||||||
chains.delete(filePath);
|
chains.delete(filePath);
|
||||||
@@ -70,7 +71,7 @@ async function waitForChain(filePath: string): Promise<void> {
|
|||||||
]);
|
]);
|
||||||
} catch {
|
} catch {
|
||||||
// Chain holder timed out or crashed — break the chain
|
// Chain holder timed out or crashed — break the chain
|
||||||
console.warn(
|
logger.warn(
|
||||||
`[gamedata-lock] Chain timeout for ${filePath}, breaking chain`,
|
`[gamedata-lock] Chain timeout for ${filePath}, breaking chain`,
|
||||||
);
|
);
|
||||||
chains.delete(filePath);
|
chains.delete(filePath);
|
||||||
|
|||||||
Reference in new issue
Block a user