From 59a45d2165b7ea846a2bda944c9b1e90b866f307 Mon Sep 17 00:00:00 2001 From: openhands Date: Sat, 17 Oct 2026 14:29:14 +0200 Subject: [PATCH] refactor: universal logger and remove N+1 catalog queries - 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 --- src/actions/catalog-items.ts | 305 ++++++++++++------ src/actions/catalog.ts | 62 +--- src/app/admin/sounds/sounds-client.tsx | 5 +- src/app/admin/sounds/trax-player.tsx | 5 +- .../admin/import/furni/batch-regen/route.ts | 6 +- src/app/api/admin/import/furni/batch/route.ts | 31 +- .../admin/import/furni/nitro-editor/route.ts | 24 +- src/app/api/admin/import/furni/route.ts | 31 +- .../catalog-manager/auto-category-dialog.tsx | 9 +- .../admin/catalog-manager/breadcrumb-bar.tsx | 5 +- .../admin/catalog-manager/sortable-tree.tsx | 5 +- src/components/admin/catalog/icon-picker.tsx | 5 +- .../admin/catalog/quick-add-furni.tsx | 7 +- src/components/admin/media-grid.tsx | 7 +- .../admin/studio/organize-imports-dialog.tsx | 13 +- src/components/public/radio-player.tsx | 7 +- src/hooks/use-server-action.ts | 5 +- src/lib/db.ts | 9 +- src/lib/logger.ts | 33 +- src/lib/services/furni-import-cache.ts | 6 +- src/lib/services/furni-import.ts | 40 +++ .../services/habbo-furnidata-cache.test.ts | 2 +- src/lib/services/habbo-furnidata-cache.ts | 4 +- src/lib/services/habboassets.ts | 2 +- src/lib/services/import-badge.ts | 12 +- src/lib/services/import/core/download.ts | 12 +- src/lib/services/import/core/gamedata-json.ts | 5 +- 27 files changed, 416 insertions(+), 241 deletions(-) diff --git a/src/actions/catalog-items.ts b/src/actions/catalog-items.ts index d5090651..f7234a32 100644 --- a/src/actions/catalog-items.ts +++ b/src/actions/catalog-items.ts @@ -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({ - 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, - }); - revalidatePath("/admin/catalog"); - } + const { created, failed } = await bulkInsertCatalogItems({ + pageId, + staffId: staff.id, + rows: rows.map((r) => ({ + baseId: r.baseId, + credits: r.credits, + points: r.points, + pointsType: r.pointsType, + amount: 1, + })), + }); 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,33 +633,11 @@ export async function translateCatalogItems(input: { nextName !== "" && nextName !== (base.publicName ?? ""); if (nameChanged) { - await db - .update(ItemsBase) - .set({ publicName: nextName }) - .where(eq(ItemsBase.id, base.id)); - const idStr = String(base.id); - const related = await db - .select({ - id: CatalogItems.id, - catalogName: CatalogItems.catalogName, - }) - .from(CatalogItems) - .where( - or( - eq(CatalogItems.itemIds, idStr), - like(CatalogItems.itemIds, `${idStr};%`), - like(CatalogItems.itemIds, `%;${idStr};%`), - like(CatalogItems.itemIds, `%;${idStr}`), - ), - ); - for (const row of related) { - if (row.catalogName !== nextName) { - await db - .update(CatalogItems) - .set({ catalogName: nextName }) - .where(eq(CatalogItems.id, row.id)); - } - } + nameUpdates.push({ id: base.id, nextName }); + catalogNameUpdates.push({ + idStr: String(base.id), + nextName, + }); namesUpdated++; } @@ -589,6 +660,40 @@ export async function translateCatalogItems(input: { } } + // Batch ItemsBase name updates. + for (const { id, nextName } of nameUpdates) { + await db + .update(ItemsBase) + .set({ publicName: nextName }) + .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, + catalogName: CatalogItems.catalogName, + }) + .from(CatalogItems) + .where( + or( + eq(CatalogItems.itemIds, idStr), + like(CatalogItems.itemIds, `${idStr};%`), + like(CatalogItems.itemIds, `%;${idStr};%`), + like(CatalogItems.itemIds, `%;${idStr}`), + ), + ); + for (const row of related) { + if (row.catalogName !== nextName) { + await db + .update(CatalogItems) + .set({ catalogName: nextName }) + .where(eq(CatalogItems.id, row.id)); + } + } + } + const furniResult = furniPatches.length > 0 ? await patchFurniEntryNames(furniPatches) diff --git a/src/actions/catalog.ts b/src/actions/catalog.ts index 1b1c1f00..0e56a36e 100644 --- a/src/actions/catalog.ts +++ b/src/actions/catalog.ts @@ -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({ - 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: "", - }); - created++; - } catch (error) { - console.error( - `[createAutoCategory] failed to insert item #${row.baseId}:`, - error, - ); - } - } + const { created } = await bulkInsertCatalogItems({ + pageId, + staffId: staff.id, + rows: rows.map((r) => ({ + baseId: r.baseId, + credits: r.credits, + points: r.points, + pointsType: r.pointsType, + amount: r.amount, + })), + }); await logStaffActivity({ staffId: staff.id, diff --git a/src/app/admin/sounds/sounds-client.tsx b/src/app/admin/sounds/sounds-client.tsx index 5a5a75cb..7ddcd0f5 100644 --- a/src/app/admin/sounds/sounds-client.tsx +++ b/src/app/admin/sounds/sounds-client.tsx @@ -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); diff --git a/src/app/admin/sounds/trax-player.tsx b/src/app/admin/sounds/trax-player.tsx index c54c3e5d..656db8b0 100644 --- a/src/app/admin/sounds/trax-player.tsx +++ b/src/app/admin/sounds/trax-player.tsx @@ -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); diff --git a/src/app/api/admin/import/furni/batch-regen/route.ts b/src/app/api/admin/import/furni/batch-regen/route.ts index ecd0fadb..796f5f0d 100644 --- a/src/app/api/admin/import/furni/batch-regen/route.ts +++ b/src/app/api/admin/import/furni/batch-regen/route.ts @@ -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}`, ); } diff --git a/src/app/api/admin/import/furni/batch/route.ts b/src/app/api/admin/import/furni/batch/route.ts index 2dd709d6..c4691374 100644 --- a/src/app/api/admin/import/furni/batch/route.ts +++ b/src/app/api/admin/import/furni/batch/route.ts @@ -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}`, ); } diff --git a/src/app/api/admin/import/furni/nitro-editor/route.ts b/src/app/api/admin/import/furni/nitro-editor/route.ts index 51e08d6c..05f382e1 100644 --- a/src/app/api/admin/import/furni/nitro-editor/route.ts +++ b/src/app/api/admin/import/furni/nitro-editor/route.ts @@ -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); }); } 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 }); diff --git a/src/app/api/admin/import/furni/route.ts b/src/app/api/admin/import/furni/route.ts index e5224e0c..b5920431 100644 --- a/src/app/api/admin/import/furni/route.ts +++ b/src/app/api/admin/import/furni/route.ts @@ -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}`, ); } diff --git a/src/components/admin/catalog-manager/auto-category-dialog.tsx b/src/components/admin/catalog-manager/auto-category-dialog.tsx index f77a5f20..c1e35361 100644 --- a/src/components/admin/catalog-manager/auto-category-dialog.tsx +++ b/src/components/admin/catalog-manager/auto-category-dialog.tsx @@ -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); diff --git a/src/components/admin/catalog-manager/breadcrumb-bar.tsx b/src/components/admin/catalog-manager/breadcrumb-bar.tsx index 159d7952..26d168a2 100644 --- a/src/components/admin/catalog-manager/breadcrumb-bar.tsx +++ b/src/components/admin/catalog-manager/breadcrumb-bar.tsx @@ -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]); diff --git a/src/components/admin/catalog-manager/sortable-tree.tsx b/src/components/admin/catalog-manager/sortable-tree.tsx index 16824d80..35f13e20 100644 --- a/src/components/admin/catalog-manager/sortable-tree.tsx +++ b/src/components/admin/catalog-manager/sortable-tree.tsx @@ -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, + }), ); } } diff --git a/src/components/admin/catalog/icon-picker.tsx b/src/components/admin/catalog/icon-picker.tsx index aca9ebad..5b99977e 100644 --- a/src/components/admin/catalog/icon-picker.tsx +++ b/src/components/admin/catalog/icon-picker.tsx @@ -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); } diff --git a/src/components/admin/catalog/quick-add-furni.tsx b/src/components/admin/catalog/quick-add-furni.tsx index 77a83a63..2ddd3cad 100644 --- a/src/components/admin/catalog/quick-add-furni.tsx +++ b/src/components/admin/catalog/quick-add-furni.tsx @@ -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]); diff --git a/src/components/admin/media-grid.tsx b/src/components/admin/media-grid.tsx index 529e0122..43695a17 100644 --- a/src/components/admin/media-grid.tsx +++ b/src/components/admin/media-grid.tsx @@ -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) { diff --git a/src/components/admin/studio/organize-imports-dialog.tsx b/src/components/admin/studio/organize-imports-dialog.tsx index 9a769292..2467dd07 100644 --- a/src/components/admin/studio/organize-imports-dialog.tsx +++ b/src/components/admin/studio/organize-imports-dialog.tsx @@ -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); diff --git a/src/components/public/radio-player.tsx b/src/components/public/radio-player.tsx index ca381f41..4d3028e2 100644 --- a/src/components/public/radio-player.tsx +++ b/src/components/public/radio-player.tsx @@ -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