diff --git a/.oxlintrc.json b/.oxlintrc.json index 4417162..0439f6a 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -11,7 +11,7 @@ "suspicious": "warn" }, "rules": { - "no-console": "warn", + "no-console": "error", "no-var": "error", "prefer-const": "error", "eqeqeq": ["error", "always"], @@ -24,18 +24,19 @@ ], "typescript/no-explicit-any": "error", "typescript/no-non-null-assertion": "error", + "typescript/no-inferrable-types": "error", "typescript/consistent-type-imports": [ "error", { "prefer": "type-imports" } ], - + "react/jsx-no-leaked-render": "error", "react/react-in-jsx-scope": "off", "react/rules-of-hooks": "error", "react/exhaustive-deps": "error", "react/jsx-key": "error", "react/no-array-index-key": "error", "react/self-closing-comp": "warn", - + "import/no-cycle": "error", "import/no-duplicates": "error", "import/no-unassigned-import": ["error", { "allow": ["**/*.css", "@testing-library/jest-dom", "@testing-library/cypress/add-commands"] @@ -51,7 +52,14 @@ "files": ["**/__tests__/**", "**/*.test.ts", "**/*.test.tsx", "cypress/**"], "rules": { "no-console": "off", - "typescript/no-explicit-any": "off" + "typescript/no-explicit-any": "off", + "typescript/no-inferrable-types": "off" + } + }, + { + "files": ["app/lib/logger.ts", "server/logger.ts"], + "rules": { + "no-console": "off" } } ], @@ -61,6 +69,7 @@ "node_modules/**", "drizzle/**", ".react-router/**", - "cypress/fixtures/**" + "cypress/fixtures/**", + "scripts/**" ] } diff --git a/app/entry.server.tsx b/app/entry.server.tsx index 6240661..77cfdf5 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -1,5 +1,6 @@ import * as Sentry from "@sentry/react-router"; import { PassThrough } from "node:stream"; +import { logger } from "~/lib/logger"; import type { AppLoadContext, EntryContext } from "react-router"; import { createReadableStreamFromReadable } from "@react-router/node"; @@ -76,7 +77,7 @@ async function handleRequest( // errors encountered during initial shell rendering since they'll // reject and get logged in handleDocumentRequest. if (shellRendered) { - console.error(error); + logger.error(error); } }, }, diff --git a/app/hooks/useDraftSocket.ts b/app/hooks/useDraftSocket.ts index 1daf146..2813160 100644 --- a/app/hooks/useDraftSocket.ts +++ b/app/hooks/useDraftSocket.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { io } from "socket.io-client"; +import { logger } from "~/lib/logger"; interface UseDraftSocketReturn { isConnected: boolean; @@ -40,7 +41,7 @@ export function useDraftSocket(seasonId: string, teamId?: string): UseDraftSocke setSocketVersion((v) => v + 1); socket.on("connect", () => { - console.log("Connected to Socket.IO:", socket.id); + logger.log("Connected to Socket.IO:", socket.id); const isReconnect = hasConnectedOnce.current; hasConnectedOnce.current = true; setIsConnected(true); @@ -53,7 +54,7 @@ export function useDraftSocket(seasonId: string, teamId?: string): UseDraftSocke }); socket.on("disconnect", (reason) => { - console.log("Disconnected from Socket.IO:", reason); + logger.log("Disconnected from Socket.IO:", reason); setIsConnected(false); if (reason === "io server disconnect") { setConnectionError("Server disconnected. Please refresh the page."); @@ -63,7 +64,7 @@ export function useDraftSocket(seasonId: string, teamId?: string): UseDraftSocke }); socket.on("connect_error", (error) => { - console.error("Socket.IO connection error:", error); + logger.error("Socket.IO connection error:", error); // Don't set connectionError here — reconnect_attempt fires immediately after // and would clear it again, causing the error overlay to flicker on every // retry. Only show a hard error once all attempts are exhausted (reconnect_failed). @@ -72,13 +73,13 @@ export function useDraftSocket(seasonId: string, teamId?: string): UseDraftSocke }); socket.io.on("reconnect_attempt", () => { - console.log("Attempting to reconnect..."); + logger.log("Attempting to reconnect..."); setIsReconnecting(true); setConnectionError(null); }); socket.io.on("reconnect_failed", () => { - console.error("Reconnection failed"); + logger.error("Reconnection failed"); setConnectionError("Failed to reconnect. Please refresh the page."); setIsReconnecting(false); }); @@ -123,7 +124,7 @@ export function useDraftSocket(seasonId: string, teamId?: string): UseDraftSocke window.removeEventListener("offline", handleOffline); window.removeEventListener("online", handleReturn); document.removeEventListener("visibilitychange", handleVisibilityChange); - console.log("Leaving draft room:", seasonId); + logger.log("Leaving draft room:", seasonId); socket.emit("leave-draft", seasonId); socket.disconnect(); }; diff --git a/app/lib/logger.ts b/app/lib/logger.ts new file mode 100644 index 0000000..896da47 --- /dev/null +++ b/app/lib/logger.ts @@ -0,0 +1,56 @@ +/** + * App-level logger. In development, delegates to console. In production, + * routes errors/warnings to Sentry and silences debug noise. + */ +import * as Sentry from "@sentry/react-router"; + +const isDev = process.env.NODE_ENV !== "production"; + +/** Splits args into the first Error found and everything else (for Sentry context). */ +function splitArgs(args: unknown[]): { err: Error | undefined; extra: unknown[] } { + const err = args.find((a): a is Error => a instanceof Error); + const extra = args.filter((a) => a !== err); + return { err, extra }; +} + +function log(...args: unknown[]): void { + if (isDev) console.log(...args); +} + +function info(...args: unknown[]): void { + if (isDev) console.info(...args); +} + +function warn(...args: unknown[]): void { + if (isDev) { + console.warn(...args); + } else { + const { err, extra } = splitArgs(args); + if (err) { + Sentry.captureException(err, { level: "warning", extra: { context: extra } }); + } else { + Sentry.captureMessage(String(args[0]), { + level: "warning", + extra: { args: extra.slice(1) }, + }); + } + } +} + +function error(...args: unknown[]): void { + if (isDev) { + console.error(...args); + } else { + const { err, extra } = splitArgs(args); + if (err) { + Sentry.captureException(err, extra.length ? { extra: { context: extra } } : undefined); + } else { + Sentry.captureMessage(String(args[0]), { + level: "error", + extra: { args: extra.slice(1) }, + }); + } + } +} + +export const logger = { log, info, warn, error }; diff --git a/app/models/draft-utils.ts b/app/models/draft-utils.ts index dab1de8..7cd4a7d 100644 --- a/app/models/draft-utils.ts +++ b/app/models/draft-utils.ts @@ -1,6 +1,7 @@ import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { eq, and, notInArray, desc, inArray, sql, asc } from "drizzle-orm"; +import { logger } from "~/lib/logger"; import type { InferSelectModel } from "drizzle-orm"; import { getTeamQueue, getAllQueuesForSeason } from "./draft-queue"; import { isParticipantDrafted, getDraftPicksWithSports, getTeamDraftPicksWithSports } from "./draft-pick"; @@ -50,7 +51,7 @@ export async function checkAndTriggerNextAutodraft(params: { if (!autodraftSettings?.isEnabled) return; - console.log( + logger.log( `[AutodraftChain] Team ${nextTeamId} has autodraft enabled, triggering immediate pick for pick ${currentPickNumber}` ); @@ -104,7 +105,7 @@ export async function autoPickForTeam( allTeams ); - console.log( + logger.log( `[AutoPick] Team ${teamId} eligible sports:`, Array.from(eligibility.eligibleSportIds) ); @@ -113,7 +114,7 @@ export async function autoPickForTeam( const queue = await getTeamQueue(teamId, db); if (queue.length > 0) { - console.log(`[AutoPick] Team ${teamId} has ${queue.length} items in queue`); + logger.log(`[AutoPick] Team ${teamId} has ${queue.length} items in queue`); // Get participant details for queue items to check eligibility const queueParticipantIds = queue.map((item) => item.participantId); @@ -134,7 +135,7 @@ export async function autoPickForTeam( for (const item of queue) { const participant = queueParticipants.find((p) => p.id === item.participantId); if (!participant) { - console.log(`[AutoPick] Queue item ${item.id} - participant not found, will remove`); + logger.log(`[AutoPick] Queue item ${item.id} - participant not found, will remove`); ineligibleQueueItemIds.push(item.id); continue; } @@ -144,7 +145,7 @@ export async function autoPickForTeam( const isDrafted = await isParticipantDrafted(seasonId, item.participantId, db); if (isDrafted) { - console.log( + logger.log( `[AutoPick] Queue item ${participant.name} (${participant.sportsSeason.sport.name}) - already drafted, will remove` ); ineligibleQueueItemIds.push(item.id); @@ -152,7 +153,7 @@ export async function autoPickForTeam( } if (!isEligible) { - console.log( + logger.log( `[AutoPick] Queue item ${participant.name} (${participant.sportsSeason.sport.name}) - not eligible for this team, will remove` ); ineligibleQueueItemIds.push(item.id); @@ -160,7 +161,7 @@ export async function autoPickForTeam( } // Found a valid pick from queue - console.log( + logger.log( `[AutoPick] Selecting from queue: ${participant.name} (${participant.sportsSeason.sport.name})` ); @@ -169,7 +170,7 @@ export async function autoPickForTeam( await db .delete(schema.draftQueue) .where(inArray(schema.draftQueue.id, ineligibleQueueItemIds)); - console.log(`[AutoPick] Removed ${ineligibleQueueItemIds.length} ineligible items from queue`); + logger.log(`[AutoPick] Removed ${ineligibleQueueItemIds.length} ineligible items from queue`); } return item.participantId; @@ -180,7 +181,7 @@ export async function autoPickForTeam( await db .delete(schema.draftQueue) .where(inArray(schema.draftQueue.id, ineligibleQueueItemIds)); - console.log( + logger.log( `[AutoPick] Removed ${ineligibleQueueItemIds.length} ineligible items from queue (all items were invalid)` ); } @@ -188,12 +189,12 @@ export async function autoPickForTeam( // Queue is empty or all queued players drafted/ineligible if (queueOnly) { - console.log(`[AutoPick] No valid queue items and queueOnly constraint is active — will not fall back to highest EV`); + logger.log(`[AutoPick] No valid queue items and queueOnly constraint is active — will not fall back to highest EV`); return null; } // Pick highest EV available from eligible sports - console.log(`[AutoPick] No valid queue items, selecting highest EV from eligible sports`); + logger.log(`[AutoPick] No valid queue items, selecting highest EV from eligible sports`); return await getTopAvailableParticipant(seasonId, eligibility.eligibleSportIds, db); } @@ -407,7 +408,7 @@ export async function pruneIneligibleQueueItems(params: { for (const item of queue) { const sportId = participantSportMap.get(item.participantId); if (sportId === undefined) { - console.warn( + logger.warn( `[QueuePrune] Team ${teamId}: queue item ${item.id} references participant ${item.participantId} not found in season sports — skipping` ); continue; @@ -424,7 +425,7 @@ export async function pruneIneligibleQueueItems(params: { const removedParticipantIds = ineligible.map((i) => i.participantId); results.push({ teamId, removedParticipantIds }); - console.log( + logger.log( `[QueuePrune] Team ${teamId}: removed ${ineligible.length} ineligible items (sport no longer eligible)` ); } @@ -500,7 +501,7 @@ export async function executeAutoPick(params: { }); if (existingPick) { - console.log(`[AutoPick] Pick ${pickNumber} already made, skipping`); + logger.log(`[AutoPick] Pick ${pickNumber} already made, skipping`); return { success: false, error: "Pick already made", @@ -553,7 +554,7 @@ export async function executeAutoPick(params: { // If queueOnly is set and queue is empty, disable autodraft and return success // (timer will fire again and pick highest EV once autodraft is disabled) if (triggeredBy === "timer" && queueOnly && autodraftSettings) { - console.log(`[AutoPick] Queue empty with queueOnly constraint — disabling autodraft for team ${teamId}`); + logger.log(`[AutoPick] Queue empty with queueOnly constraint — disabling autodraft for team ${teamId}`); await db .update(schema.autodraftSettings) .set({ isEnabled: false, updatedAt: new Date() }) @@ -567,7 +568,7 @@ export async function executeAutoPick(params: { queueOnly: autodraftSettings.queueOnly, }); } catch (error) { - console.error("[AutoPick] Socket.IO autodraft-updated error (queue-empty shutoff):", error); + logger.error("[AutoPick] Socket.IO autodraft-updated error (queue-empty shutoff):", error); } return { success: true }; @@ -615,7 +616,7 @@ export async function executeAutoPick(params: { }); if (!currentTimer) { - console.warn(`[AutoPick] No timer found for team ${teamId} in season ${seasonId}`); + logger.warn(`[AutoPick] No timer found for team ${teamId} in season ${seasonId}`); } // Create the draft pick @@ -635,7 +636,7 @@ export async function executeAutoPick(params: { }) .returning(); - console.log( + logger.log( `[AutoPick] Pick created - ${triggeredBy} triggered - Pick ${pickNumber} - Participant ${participantId}` ); @@ -661,13 +662,13 @@ export async function executeAutoPick(params: { ) .returning(); emitTimeRemaining = updatedTimer?.timeRemaining ?? incrementTime; - console.log( + logger.log( `[AutoPick] Reset timer for team ${teamId} to ${emitTimeRemaining}s (standard mode)` ); } else { // Chess clock: no DB update — auto picks don't earn increment. emitTimeRemaining = currentTimer?.timeRemaining ?? 0; - console.log( + logger.log( `[AutoPick] Chess clock pick for team ${teamId}, bank frozen at ${emitTimeRemaining}s (no increment)` ); } @@ -680,7 +681,7 @@ export async function executeAutoPick(params: { currentPickNumber: nextPickNumber, }); } catch (error) { - console.error("[AutoPick] Socket.IO timer-update error:", error); + logger.error("[AutoPick] Socket.IO timer-update error:", error); } // Next team's timer is unchanged — their bank carries forward as-is @@ -721,12 +722,12 @@ export async function executeAutoPick(params: { }); } } catch (error) { - console.error("[AutoPick] Error pruning ineligible queue items:", error); + logger.error("[AutoPick] Error pruning ineligible queue items:", error); } // Handle autodraft settings for timer-based picks with "next_pick" mode if (triggeredBy === "timer" && autodraftSettings?.isEnabled && autodraftSettings.mode === "next_pick") { - console.log(`[AutoPick] Disabling autodraft for team ${teamId} after next_pick`); + logger.log(`[AutoPick] Disabling autodraft for team ${teamId} after next_pick`); await db .update(schema.autodraftSettings) .set({ @@ -744,7 +745,7 @@ export async function executeAutoPick(params: { queueOnly: autodraftSettings.queueOnly, }); } catch (error) { - console.error("[AutoPick] Socket.IO autodraft-updated error:", error); + logger.error("[AutoPick] Socket.IO autodraft-updated error:", error); } } @@ -778,7 +779,7 @@ export async function executeAutoPick(params: { io.to(`draft-${seasonId}`).emit("draft-completed"); } } catch (error) { - console.error("[AutoPick] Socket.IO events error:", error); + logger.error("[AutoPick] Socket.IO events error:", error); } // Check if next team has autodraft enabled and trigger immediately @@ -801,7 +802,7 @@ export async function executeAutoPick(params: { isDraftComplete, }; } catch (error) { - console.error("[AutoPick] Error in executeAutoPick:", error); + logger.error("[AutoPick] Error in executeAutoPick:", error); return { success: false, error: error instanceof Error ? error.message : "Unknown error", diff --git a/app/models/scoring-calculator.ts b/app/models/scoring-calculator.ts index 07f4b5a..7aaf20d 100644 --- a/app/models/scoring-calculator.ts +++ b/app/models/scoring-calculator.ts @@ -8,6 +8,7 @@ import { sendStandingsUpdateNotification, type ScoredMatch } from "~/services/di import { BRACKET_TEMPLATES } from "~/lib/bracket-templates"; import { getUserDisplayName } from "~/models/user"; import { createDailySnapshot } from "~/models/standings"; +import { logger } from "~/lib/logger"; /** * Core scoring calculation engine @@ -249,7 +250,7 @@ export async function processPlayoffEvent( if (config === null) { // Unrecognized scoring round: losers earn 0 pts (outside top-8). - console.warn( + logger.warn( `[ScoringCalculator] Unrecognized scoring round "${round}" for event ${eventId}. Losers receive 0 pts.` ); for (const match of matches) { @@ -312,11 +313,11 @@ export async function processPlayoffEvent( // Auto-trigger probability recalculation after result try { await updateProbabilitiesAfterResult(event.sportsSeasonId, true); - console.log( + logger.log( `[ScoringCalculator] Auto-updated probabilities for sports season ${event.sportsSeasonId}` ); } catch (error) { - console.error( + logger.error( `[ScoringCalculator] Failed to auto-update probabilities for sports season ${event.sportsSeasonId}:`, error ); @@ -369,7 +370,7 @@ export async function processMatchResult( if (config === null) { // Unrecognized scoring round: loser earns 0 pts, winner gets fallback T5–T8 floor. - console.warn( + logger.warn( `[ScoringCalculator] processMatchResult: unrecognized round "${round}". Loser receives 0 pts.` ); await upsertParticipantResult(loserId, sportsSeasonId, 0, db); @@ -393,7 +394,7 @@ export async function processMatchResult( try { await updateProbabilitiesAfterResult(sportsSeasonId, true); } catch (error) { - console.error( + logger.error( `[ScoringCalculator] Failed to auto-update probabilities for sports season ${sportsSeasonId}:`, error ); @@ -557,7 +558,7 @@ export async function processQualifyingEvent( .where(eq(schema.sportsSeasons.id, event.sportsSeasonId)); } - console.log( + logger.log( `[ScoringCalculator] Processed qualifying event ${eventId}: awarded QP to ${results.length} participants` ); } @@ -677,17 +678,17 @@ export async function finalizeQualifyingPoints( // Auto-trigger probability recalculation after result try { await updateProbabilitiesAfterResult(sportsSeasonId, true); - console.log( + logger.log( `[ScoringCalculator] Auto-updated probabilities for sports season ${sportsSeasonId}` ); } catch (error) { - console.error( + logger.error( `[ScoringCalculator] Failed to auto-update probabilities for sports season ${sportsSeasonId}:`, error ); } - console.log( + logger.log( `[ScoringCalculator] Finalized qualifying points for sports season ${sportsSeasonId}: ${standings.length} participants processed` ); } @@ -787,17 +788,17 @@ export async function processSeasonStandings( // Auto-trigger probability recalculation after result try { await updateProbabilitiesAfterResult(sportsSeasonId, true); - console.log( + logger.log( `[ScoringCalculator] Auto-updated probabilities for sports season ${sportsSeasonId}` ); } catch (error) { - console.error( + logger.error( `[ScoringCalculator] Failed to auto-update probabilities for sports season ${sportsSeasonId}:`, error ); } - console.log( + logger.log( `[ScoringCalculator] Processed season standings for sports season ${sportsSeasonId}` ); } @@ -1208,7 +1209,7 @@ export async function recalculateStandings( } } - console.log(`[ScoringCalculator] Recalculated standings for season ${seasonId}: ${teams.length} teams ranked`); + logger.log(`[ScoringCalculator] Recalculated standings for season ${seasonId}: ${teams.length} teams ranked`); } /** @@ -1289,7 +1290,7 @@ export async function recalculateAffectedLeagues( try { await createDailySnapshot(seasonId, db); } catch (err) { - console.error(`[ScoringCalculator] Snapshot failed for season ${seasonId}:`, err); + logger.error(`[ScoringCalculator] Snapshot failed for season ${seasonId}:`, err); } // Check if any team's score changed — if so, send Discord notification @@ -1413,11 +1414,11 @@ export async function recalculateAffectedLeagues( }); } catch (err) { // Log but don't fail the scoring pipeline if Discord is unreachable - console.error(`[ScoringCalculator] Discord notification failed for season ${seasonId}:`, err); + logger.error(`[ScoringCalculator] Discord notification failed for season ${seasonId}:`, err); } } - console.log( + logger.log( `[ScoringCalculator] Recalculated ${seasonIds.length} affected season(s) for sports season ${sportsSeasonId}` ); } diff --git a/app/models/standings.ts b/app/models/standings.ts index 4f28553..abf5104 100644 --- a/app/models/standings.ts +++ b/app/models/standings.ts @@ -3,6 +3,7 @@ import * as schema from "~/database/schema"; import { eq, and } from "drizzle-orm"; import type { TeamStanding, TeamStandingSnapshot, TeamStandingWithChange } from "~/types/standings"; import { calculateBracketPoints, calculateFantasyPoints } from "~/models/scoring-rules"; +import { logger } from "~/lib/logger"; // Re-export types from shared types file export type { TeamStanding, TeamStandingSnapshot, TeamStandingWithChange } from "~/types/standings"; @@ -374,7 +375,7 @@ export async function createDailySnapshot( } }); - console.log(`[Standings] Upserted daily snapshot for season ${seasonId}`); + logger.log(`[Standings] Upserted daily snapshot for season ${seasonId}`); } /** diff --git a/app/routes/admin.data-sync.tsx b/app/routes/admin.data-sync.tsx index 69557e8..518f41a 100644 --- a/app/routes/admin.data-sync.tsx +++ b/app/routes/admin.data-sync.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { useFetcher } from "react-router"; import type { Route } from "./+types/admin.data-sync"; +import { logger } from "~/lib/logger"; import { findAllSports } from "~/models/sport"; import { findAllSportsSeasons } from "~/models/sports-season"; import { findAllSeasonTemplates } from "~/models/season-template"; @@ -77,7 +78,7 @@ export async function action({ request }: Route.ActionArgs) { message: `Import complete! Created: ${result.created}, Updated: ${result.updated}, Skipped: ${result.skipped}`, }; } catch (error) { - console.error("Import error:", error); + logger.error("Import error:", error); return { success: false, error: error instanceof Error ? error.message : "Import failed", @@ -116,7 +117,7 @@ export default function DataSync({ loaderData }: Route.ComponentProps) { document.body.removeChild(a); URL.revokeObjectURL(url); } catch (error) { - console.error("Export failed:", error); + logger.error("Export failed:", error); alert("Export failed. Please try again."); } finally { setIsExporting(false); diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts index 1ce9a01..b5bda7b 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts @@ -40,6 +40,7 @@ import { getAdvancingParticipantIds, getEliminatedParticipantIds, } from "~/models/tournament-group"; +import { logger } from "~/lib/logger"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { eq } from "drizzle-orm"; @@ -173,7 +174,7 @@ export async function action({ request, params }: Route.ActionArgs) { } } - console.log(`[BracketGeneration] Marked ${allParticipants.length - participantIds.length} participants as eliminated`); + logger.log(`[BracketGeneration] Marked ${allParticipants.length - participantIds.length} participants as eliminated`); } // Update the event to store the template ID, scoring start round, and region config @@ -185,7 +186,7 @@ export async function action({ request, params }: Route.ActionArgs) { return { success: "Bracket generated successfully" }; } catch (error) { - console.error("Error generating bracket:", error); + logger.error("Error generating bracket:", error); return { error: error instanceof Error @@ -223,7 +224,7 @@ export async function action({ request, params }: Route.ActionArgs) { .where(eq(schema.scoringEvents.id, eventId)); await processPlayoffEvent(eventId, db, { skipRecalculate: true }); - console.log(`[AutoComplete] Round "${round}" auto-completed for event ${eventId}`); + logger.log(`[AutoComplete] Round "${round}" auto-completed for event ${eventId}`); } if (intent === "set-winner") { @@ -275,7 +276,7 @@ export async function action({ request, params }: Route.ActionArgs) { error instanceof Error && error.message.includes("already filled") ) { - console.warn("Match already filled, skipping advancement"); + logger.warn("Match already filled, skipping advancement"); } else { throw error; // Re-throw unexpected errors } @@ -303,7 +304,7 @@ export async function action({ request, params }: Route.ActionArgs) { return { success: "Winner set successfully" }; } catch (error) { - console.error("Error setting winner:", error); + logger.error("Error setting winner:", error); return { error: error instanceof Error ? error.message : "Failed to set winner", @@ -384,7 +385,7 @@ export async function action({ request, params }: Route.ActionArgs) { error instanceof Error && error.message.includes("already filled") ) { - console.warn("Match already filled, skipping advancement"); + logger.warn("Match already filled, skipping advancement"); } else { throw error; } @@ -408,7 +409,7 @@ export async function action({ request, params }: Route.ActionArgs) { successCount++; processedMatchIds.push(matchId); } catch (error) { - console.error(`Error setting winner for match ${matchId}:`, error); + logger.error(`Error setting winner for match ${matchId}:`, error); errors.push( `Match ${matchId}: ${error instanceof Error ? error.message : "Unknown error"}` ); @@ -424,7 +425,7 @@ export async function action({ request, params }: Route.ActionArgs) { try { await updateProbabilitiesAfterResult(event.sportsSeasonId, true); } catch (error) { - console.error(`Error updating probabilities after batch round winners:`, error); + logger.error(`Error updating probabilities after batch round winners:`, error); } await autoCompleteRoundIfDone(event.id, round, event.sportsSeasonId, db); } @@ -442,7 +443,7 @@ export async function action({ request, params }: Route.ActionArgs) { return { success: `Successfully set ${successCount} winner(s) for ${round}` }; } catch (error) { - console.error("Error setting round winners:", error); + logger.error("Error setting round winners:", error); return { error: error instanceof Error ? error.message : "Failed to set winners", @@ -545,7 +546,7 @@ export async function action({ request, params }: Route.ActionArgs) { success: `${round} completed and placements calculated successfully`, }; } catch (error) { - console.error("Error completing round:", error); + logger.error("Error completing round:", error); return { error: error instanceof Error ? error.message : "Failed to complete round", @@ -591,13 +592,13 @@ export async function action({ request, params }: Route.ActionArgs) { } } - console.log(`[ReprocessEliminations] Marked ${eliminatedCount} participants as eliminated`); + logger.log(`[ReprocessEliminations] Marked ${eliminatedCount} participants as eliminated`); return { success: `Successfully marked ${eliminatedCount} participant(s) as eliminated`, }; } catch (error) { - console.error("Error reprocessing eliminations:", error); + logger.error("Error reprocessing eliminations:", error); return { error: error instanceof Error ? error.message : "Failed to reprocess eliminations", @@ -685,7 +686,7 @@ export async function action({ request, params }: Route.ActionArgs) { success: `Bracket finalized! All placements calculated and standings updated.`, }; } catch (error) { - console.error("Error finalizing bracket:", error); + logger.error("Error finalizing bracket:", error); return { error: error instanceof Error ? error.message : "Failed to finalize bracket", @@ -749,7 +750,7 @@ export async function action({ request, params }: Route.ActionArgs) { return { success: "Groups and knockout bracket structure created successfully" }; } catch (error) { - console.error("Error generating groups:", error); + logger.error("Error generating groups:", error); return { error: error instanceof Error ? error.message : "Failed to generate groups", }; @@ -771,7 +772,7 @@ export async function action({ request, params }: Route.ActionArgs) { : "Team reinstated", }; } catch (error) { - console.error("Error toggling elimination:", error); + logger.error("Error toggling elimination:", error); return { error: error instanceof Error ? error.message : "Failed to toggle elimination", }; @@ -831,13 +832,13 @@ export async function action({ request, params }: Route.ActionArgs) { await setParticipantResult(participantId, event.sportsSeasonId, 0); } - console.log( + logger.log( `[PopulateKnockout] Assigned 32 participants to knockout, marked ${eliminatedIds.length} as eliminated` ); return { success: "Knockout bracket populated successfully" }; } catch (error) { - console.error("Error populating knockout:", error); + logger.error("Error populating knockout:", error); return { error: error instanceof Error ? error.message : "Failed to populate knockout", }; diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts b/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts index 7468fa1..9bc792a 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts @@ -1,4 +1,5 @@ import type { Route } from "./+types/admin.sports-seasons.$id.events.$eventId"; +import { logger } from "~/lib/logger"; import { findSportsSeasonById } from "~/models/sports-season"; import { findParticipantsBySportsSeasonId } from "~/models/participant"; import { @@ -93,7 +94,7 @@ export async function action({ request, params }: Route.ActionArgs) { return { success: "Event marked as qualifying event!" }; } catch (error) { - console.error("Error marking event as qualifying:", error); + logger.error("Error marking event as qualifying:", error); return { error: "Failed to mark event as qualifying" }; } } @@ -103,7 +104,7 @@ export async function action({ request, params }: Route.ActionArgs) { await processQualifyingEvent(params.eventId); return { success: "Qualifying points processed and awarded!" }; } catch (error) { - console.error("Error processing qualifying points:", error); + logger.error("Error processing qualifying points:", error); return { error: error instanceof Error ? error.message : "Failed to process qualifying points" }; } } @@ -131,7 +132,7 @@ export async function action({ request, params }: Route.ActionArgs) { return { success: "Event marked as completed" }; } catch (error) { - console.error("Error completing event:", error); + logger.error("Error completing event:", error); return { error: "Failed to complete event" }; } } @@ -141,7 +142,7 @@ export async function action({ request, params }: Route.ActionArgs) { await updateScoringEvent(params.eventId, { isComplete: false }); return { success: "Event marked as not updated" }; } catch (error) { - console.error("Error uncompleting event:", error); + logger.error("Error uncompleting event:", error); return { error: "Failed to update event" }; } } @@ -178,7 +179,7 @@ export async function action({ request, params }: Route.ActionArgs) { }); return { success: "Event updated" }; } catch (error) { - console.error("Error updating event:", error); + logger.error("Error updating event:", error); return { error: "Failed to update event" }; } } @@ -210,7 +211,7 @@ export async function action({ request, params }: Route.ActionArgs) { await createEventResult(resultData); return { success: "Result added successfully" }; } catch (error) { - console.error("Error adding result:", error); + logger.error("Error adding result:", error); return { error: "Failed to add result" }; } } @@ -240,7 +241,7 @@ export async function action({ request, params }: Route.ActionArgs) { await updateEventResult(resultId, updateData); return { success: "Result updated successfully" }; } catch (error) { - console.error("Error updating result:", error); + logger.error("Error updating result:", error); return { error: "Failed to update result" }; } } @@ -256,7 +257,7 @@ export async function action({ request, params }: Route.ActionArgs) { await deleteEventResult(resultId); return { success: "Result deleted successfully" }; } catch (error) { - console.error("Error deleting result:", error); + logger.error("Error deleting result:", error); return { error: "Failed to delete result" }; } } @@ -317,7 +318,7 @@ export async function action({ request, params }: Route.ActionArgs) { return { success: `Updated standings for ${participantPoints.length} participants (positions auto-calculated from points)` }; } catch (error) { - console.error("Error updating season standings:", error); + logger.error("Error updating season standings:", error); return { error: "Failed to update season standings" }; } } diff --git a/app/routes/admin.sports-seasons.$id.events.server.ts b/app/routes/admin.sports-seasons.$id.events.server.ts index f00432f..ff103ac 100644 --- a/app/routes/admin.sports-seasons.$id.events.server.ts +++ b/app/routes/admin.sports-seasons.$id.events.server.ts @@ -1,5 +1,6 @@ import type { Route } from "./+types/admin.sports-seasons.$id.events"; import { redirect } from "react-router"; +import { logger } from "~/lib/logger"; import { findSportsSeasonById } from "~/models/sports-season"; import { getScoringEventsForSportsSeason, @@ -52,7 +53,7 @@ export async function action({ request, params }: Route.ActionArgs) { await finalizeQualifyingPoints(params.id); return { success: "Qualifying points finalized and fantasy placements assigned to top 8!" }; } catch (error) { - console.error("Error finalizing qualifying points:", error); + logger.error("Error finalizing qualifying points:", error); return { error: error instanceof Error ? error.message : "Failed to finalize qualifying points" }; } } @@ -110,7 +111,7 @@ export async function action({ request, params }: Route.ActionArgs) { await bulkCreateScoringEvents(params.id, validEvents); return { success: `Created ${validEvents.length} event${validEvents.length !== 1 ? "s" : ""} successfully` }; } catch (error) { - console.error("Error bulk creating events:", error); + logger.error("Error bulk creating events:", error); return { error: "Failed to create events. Please try again." }; } } @@ -126,7 +127,7 @@ export async function action({ request, params }: Route.ActionArgs) { await deleteScoringEvent(eventId); return { success: "Event deleted successfully" }; } catch (error) { - console.error("Error deleting event:", error); + logger.error("Error deleting event:", error); return { error: "Failed to delete event" }; } } @@ -181,7 +182,7 @@ export async function action({ request, params }: Route.ActionArgs) { const event = await createScoringEvent(eventData); return redirect(`/admin/sports-seasons/${params.id}/events/${event.id}`); } catch (error) { - console.error("Error creating scoring event:", error); + logger.error("Error creating scoring event:", error); return { error: "Failed to create scoring event. Please try again." }; } } diff --git a/app/routes/admin.sports-seasons.$id.expected-values.server.ts b/app/routes/admin.sports-seasons.$id.expected-values.server.ts index 303ca44..5552e64 100644 --- a/app/routes/admin.sports-seasons.$id.expected-values.server.ts +++ b/app/routes/admin.sports-seasons.$id.expected-values.server.ts @@ -1,4 +1,5 @@ import type { Route } from "./+types/admin.sports-seasons.$id.expected-values"; +import { logger } from "~/lib/logger"; import { findSportsSeasonById } from "~/models/sports-season"; import { findParticipantsBySportsSeasonId } from "~/models/participant"; import { @@ -78,7 +79,7 @@ export async function action({ request, params }: Route.ActionArgs) { return { success: true, totalEV }; } catch (error) { - console.error("Error saving probabilities:", error); + logger.error("Error saving probabilities:", error); return { error: error instanceof Error ? error.message : "Failed to save probabilities" }; diff --git a/app/routes/admin.sports-seasons.$id.futures-odds.tsx b/app/routes/admin.sports-seasons.$id.futures-odds.tsx index 4ef70ef..dd02a22 100644 --- a/app/routes/admin.sports-seasons.$id.futures-odds.tsx +++ b/app/routes/admin.sports-seasons.$id.futures-odds.tsx @@ -1,6 +1,7 @@ import { Form, redirect, useLoaderData, useActionData, useNavigation } from 'react-router'; import type { Route } from './+types/admin.sports-seasons.$id.futures-odds'; +import { logger } from '~/lib/logger'; import { findSportsSeasonById, updateSportsSeason } from '~/models/sports-season'; import { findParticipantsBySportsSeasonId } from '~/models/participant'; import { getAllParticipantEVsForSeason, batchSaveSourceOdds, batchUpsertParticipantEVs } from '~/models/participant-expected-value'; @@ -155,7 +156,7 @@ export async function action({ request, params }: Route.ActionArgs) { await updateSportsSeason(sportsSeasonId, { simulationStatus: 'idle' }); } catch (error) { await updateSportsSeason(sportsSeasonId, { simulationStatus: 'failed' }); - console.error('Error running simulation:', error); + logger.error('Error running simulation:', error); return { success: false, message: error instanceof Error ? error.message : 'Simulation failed', diff --git a/app/routes/admin.sports-seasons.$id.participants.tsx b/app/routes/admin.sports-seasons.$id.participants.tsx index f8183dc..a842b02 100644 --- a/app/routes/admin.sports-seasons.$id.participants.tsx +++ b/app/routes/admin.sports-seasons.$id.participants.tsx @@ -1,6 +1,7 @@ import { Form, Link } from "react-router"; import type { Route } from "./+types/admin.sports-seasons.$id.participants"; +import { logger } from "~/lib/logger"; import { findSportsSeasonById } from "~/models/sports-season"; import { findParticipantsBySportsSeasonId, @@ -90,7 +91,7 @@ export async function action({ request, params }: Route.ActionArgs) { return { success: true, count: names.length }; } catch (error) { - console.error("Error creating participants:", error); + logger.error("Error creating participants:", error); return { error: "Failed to add participants. Please try again." }; } } @@ -113,7 +114,7 @@ export async function action({ request, params }: Route.ActionArgs) { return { success: true }; } catch (error) { - console.error("Error creating participant:", error); + logger.error("Error creating participant:", error); return { error: "Failed to add participant. Please try again." }; } } diff --git a/app/routes/admin.sports-seasons.$id.regular-standings.tsx b/app/routes/admin.sports-seasons.$id.regular-standings.tsx index 5e6c84d..7561f82 100644 --- a/app/routes/admin.sports-seasons.$id.regular-standings.tsx +++ b/app/routes/admin.sports-seasons.$id.regular-standings.tsx @@ -1,6 +1,7 @@ import { Form, Link } from "react-router"; import type { Route } from "./+types/admin.sports-seasons.$id.regular-standings"; +import { logger } from "~/lib/logger"; import { findSportsSeasonById } from "~/models/sports-season"; import { findParticipantsBySportsSeasonId } from "~/models/participant"; import { getRegularSeasonStandings, upsertManualStanding } from "~/models/regular-season-standings"; @@ -103,7 +104,7 @@ export async function action({ request, params }: Route.ActionArgs) { } return { success: true }; } catch (error) { - console.error("Error saving regular season standings:", error); + logger.error("Error saving regular season standings:", error); return { error: "Failed to save standings. Please try again." }; } } diff --git a/app/routes/admin.sports-seasons.$id.standings.tsx b/app/routes/admin.sports-seasons.$id.standings.tsx index 98b1717..7b1cf81 100644 --- a/app/routes/admin.sports-seasons.$id.standings.tsx +++ b/app/routes/admin.sports-seasons.$id.standings.tsx @@ -1,6 +1,7 @@ import { Form, Link } from "react-router"; import type { Route } from "./+types/admin.sports-seasons.$id.standings"; +import { logger } from "~/lib/logger"; import { findSportsSeasonById } from "~/models/sports-season"; import { findParticipantsBySportsSeasonId } from "~/models/participant"; import { getSeasonResults, upsertSeasonResultsBulk } from "~/models/participant-season-result"; @@ -74,7 +75,7 @@ export async function action({ request, params }: Route.ActionArgs) { await upsertSeasonResultsBulk(updates); return { success: true }; } catch (error) { - console.error("Error updating standings:", error); + logger.error("Error updating standings:", error); return { error: "Failed to save standings. Please try again." }; } } diff --git a/app/routes/admin.sports-seasons.$id.tsx b/app/routes/admin.sports-seasons.$id.tsx index b6a19a7..a374445 100644 --- a/app/routes/admin.sports-seasons.$id.tsx +++ b/app/routes/admin.sports-seasons.$id.tsx @@ -3,6 +3,7 @@ import { getAuth } from "@clerk/react-router/server"; import { isUserAdminByClerkId } from "~/models/user"; import type { Route } from "./+types/admin.sports-seasons.$id"; +import { logger } from "~/lib/logger"; import { findSportsSeasonById, updateSportsSeason, deleteSportsSeason, type NewSportsSeason } from "~/models/sports-season"; import { processSeasonStandings, recalculateStandings } from "~/models/scoring-calculator"; import { createDailySnapshot } from "~/models/standings"; @@ -127,7 +128,7 @@ export async function action(args: Route.ActionArgs) { await Promise.all(links.map((link) => createDailySnapshot(link.seasonId, db))); return { success: true, intent: "rescore", message: `Rescored ${links.length} linked season(s).` }; } catch (error) { - console.error("Error rescoring:", error); + logger.error("Error rescoring:", error); return { error: "Failed to rescore. Please try again." }; } } @@ -137,7 +138,7 @@ export async function action(args: Route.ActionArgs) { const result = await syncStandings(params.id); return { success: true, intent: "sync-standings", syncResult: result }; } catch (error) { - console.error("Error syncing standings:", error); + logger.error("Error syncing standings:", error); return { syncError: error instanceof Error ? error.message : "Failed to sync standings. Please try again.", @@ -197,7 +198,7 @@ export async function action(args: Route.ActionArgs) { return { success: true, intent: "resolve-mapping", resolvedTeam: String(standingData.teamName ?? "") }; } catch (error) { - console.error("Error resolving mapping:", error); + logger.error("Error resolving mapping:", error); return { error: "Failed to resolve mapping. Please try again." }; } } @@ -208,7 +209,7 @@ export async function action(args: Route.ActionArgs) { await updateSportsSeason(params.id, { status: "completed" }); return { success: true, intent: "finalize-standings", message: "Standings finalized and fantasy placements assigned!" }; } catch (error) { - console.error("Error finalizing standings:", error); + logger.error("Error finalizing standings:", error); return { error: "Failed to finalize standings. Please try again." }; } } @@ -277,7 +278,7 @@ export async function action(args: Route.ActionArgs) { return { success: true, message: "Sports season updated successfully!" }; } catch (error) { - console.error("Error updating sports season:", error); + logger.error("Error updating sports season:", error); return { error: "Failed to update sports season. Please try again." }; } } diff --git a/app/routes/admin.sports-seasons.new.tsx b/app/routes/admin.sports-seasons.new.tsx index 701cd4d..7b7503e 100644 --- a/app/routes/admin.sports-seasons.new.tsx +++ b/app/routes/admin.sports-seasons.new.tsx @@ -1,6 +1,7 @@ import { Form, Link, redirect } from "react-router"; import type { Route } from "./+types/admin.sports-seasons.new"; +import { logger } from "~/lib/logger"; import { createSportsSeason, type NewSportsSeason } from "~/models/sports-season"; import { findAllSports } from "~/models/sport"; import { Button } from "~/components/ui/button"; @@ -100,7 +101,7 @@ export async function action({ request }: Route.ActionArgs) { return redirect("/admin/sports-seasons"); } catch (error) { - console.error("Error creating sports season:", error); + logger.error("Error creating sports season:", error); return { error: "Failed to create sports season. Please try again." }; } } diff --git a/app/routes/admin.sports.$id.tsx b/app/routes/admin.sports.$id.tsx index 4c7ff9b..6ea013f 100644 --- a/app/routes/admin.sports.$id.tsx +++ b/app/routes/admin.sports.$id.tsx @@ -1,6 +1,7 @@ import { Form, Link, redirect } from "react-router"; import type { Route } from "./+types/admin.sports.$id"; +import { logger } from "~/lib/logger"; import { findSportById, updateSport } from "~/models/sport"; import { writeFile, mkdir } from "node:fs/promises"; import { join } from "node:path"; @@ -94,7 +95,7 @@ export async function action({ request, params }: Route.ActionArgs) { const buffer = Buffer.from(bytes); await writeFile(filepath, buffer); } catch (error) { - console.error("Error saving icon file:", error); + logger.error("Error saving icon file:", error); return { error: "Failed to save icon file" }; } } else if (keepExistingIcon !== "true") { @@ -120,7 +121,7 @@ export async function action({ request, params }: Route.ActionArgs) { return redirect("/admin/sports"); } catch (error) { - console.error("Error updating sport:", error); + logger.error("Error updating sport:", error); const message = error instanceof Error ? error.message : String(error); const isUniqueViolation = message.includes("unique") || message.includes("duplicate"); return { diff --git a/app/routes/admin.sports.new.tsx b/app/routes/admin.sports.new.tsx index 816776b..e0fcac9 100644 --- a/app/routes/admin.sports.new.tsx +++ b/app/routes/admin.sports.new.tsx @@ -1,6 +1,7 @@ import { Form, Link, redirect } from "react-router"; import type { Route } from "./+types/admin.sports.new"; +import { logger } from "~/lib/logger"; import { createSport } from "~/models/sport"; import { writeFile, mkdir } from "node:fs/promises"; import { join } from "node:path"; @@ -79,7 +80,7 @@ export async function action({ request }: Route.ActionArgs) { const buffer = Buffer.from(bytes); await writeFile(filepath, buffer); } catch (error) { - console.error("Error saving icon file:", error); + logger.error("Error saving icon file:", error); return { error: "Failed to save icon file" }; } } @@ -95,7 +96,7 @@ export async function action({ request }: Route.ActionArgs) { return redirect("/admin/sports"); } catch (error) { - console.error("Error creating sport:", error); + logger.error("Error creating sport:", error); return { error: "Failed to create sport. The slug might already exist." }; } } diff --git a/app/routes/admin.standings-snapshots.tsx b/app/routes/admin.standings-snapshots.tsx index 5f34883..424be43 100644 --- a/app/routes/admin.standings-snapshots.tsx +++ b/app/routes/admin.standings-snapshots.tsx @@ -1,6 +1,7 @@ import { Form } from "react-router"; import type { Route } from "./+types/admin.standings-snapshots"; +import { logger } from "~/lib/logger"; import { database } from "~/database/context"; import { eq, or } from "drizzle-orm"; import * as schema from "~/database/schema"; @@ -104,7 +105,7 @@ export async function action({ request }: Route.ActionArgs) { message: "Invalid intent", }; } catch (error) { - console.error("Error creating snapshot:", error); + logger.error("Error creating snapshot:", error); return { success: false, message: error instanceof Error ? error.message : "Unknown error", diff --git a/app/routes/admin.templates.$id.tsx b/app/routes/admin.templates.$id.tsx index eac016c..1c091ad 100644 --- a/app/routes/admin.templates.$id.tsx +++ b/app/routes/admin.templates.$id.tsx @@ -2,7 +2,8 @@ import { Form, Link, redirect } from "react-router"; import { useState } from "react"; import type { Route } from "./+types/admin.templates.$id"; -import { +import { logger } from "~/lib/logger"; +import { findSeasonTemplateWithSportsSeasons, updateSeasonTemplate, deleteSeasonTemplate, @@ -125,7 +126,7 @@ export async function action({ request, params }: Route.ActionArgs) { return { success: true }; } catch (error) { - console.error("Error updating template:", error); + logger.error("Error updating template:", error); return { error: "Failed to update template. Please try again." }; } } diff --git a/app/routes/admin.templates.new.tsx b/app/routes/admin.templates.new.tsx index 41052d7..13c4465 100644 --- a/app/routes/admin.templates.new.tsx +++ b/app/routes/admin.templates.new.tsx @@ -1,6 +1,7 @@ import { Form, Link, redirect } from "react-router"; import type { Route } from "./+types/admin.templates.new"; +import { logger } from "~/lib/logger"; import { createSeasonTemplate } from "~/models/season-template"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; @@ -48,7 +49,7 @@ export async function action({ request }: Route.ActionArgs) { return redirect(`/admin/templates/${template.id}`); } catch (error) { - console.error("Error creating template:", error); + logger.error("Error creating template:", error); return { error: "Failed to create template. Please try again." }; } } diff --git a/app/routes/api/autodraft.update.ts b/app/routes/api/autodraft.update.ts index a3c4902..ce28978 100644 --- a/app/routes/api/autodraft.update.ts +++ b/app/routes/api/autodraft.update.ts @@ -5,6 +5,7 @@ import { eq, and, asc } from "drizzle-orm"; import { getSocketIO } from "~/server/socket"; import { isUserAdminByClerkId } from "~/models/user"; import { getTeamForPick } from "~/lib/draft-order"; +import { logger } from "~/lib/logger"; import type { ActionFunctionArgs } from "react-router"; export async function action(args: ActionFunctionArgs) { @@ -131,7 +132,7 @@ export async function action(args: ActionFunctionArgs) { db, }); }).catch((err) => { - console.error("[AutodraftUpdate] Mid-turn autopick failed:", err); + logger.error("[AutodraftUpdate] Mid-turn autopick failed:", err); }); } diff --git a/app/routes/api/draft.adjust-time-bank.ts b/app/routes/api/draft.adjust-time-bank.ts index be549d3..e634b7a 100644 --- a/app/routes/api/draft.adjust-time-bank.ts +++ b/app/routes/api/draft.adjust-time-bank.ts @@ -4,6 +4,7 @@ import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { isCommissioner } from "~/models/commissioner"; import { getSocketIO } from "../../../server/socket"; +import { logger } from "~/lib/logger"; import type { ActionFunctionArgs } from "react-router"; export async function action(args: ActionFunctionArgs) { @@ -85,7 +86,7 @@ export async function action(args: ActionFunctionArgs) { currentPickNumber: season.currentPickNumber ?? 1, }); } catch (error) { - console.error("Socket.IO error:", error); + logger.error("Socket.IO error:", error); } return Response.json({ success: true, timeRemaining: newTime }); diff --git a/app/routes/api/draft.force-manual-pick.ts b/app/routes/api/draft.force-manual-pick.ts index f920509..2767be1 100644 --- a/app/routes/api/draft.force-manual-pick.ts +++ b/app/routes/api/draft.force-manual-pick.ts @@ -9,6 +9,7 @@ import { getParticipantsForSeasonWithSports } from "~/models/participant"; import { getSeasonSportsSimple } from "~/models/season-sport"; import { calculatePickInfo, checkAndTriggerNextAutodraft } from "~/models/draft-utils"; import { getSocketIO } from "../../../server/socket"; +import { logger } from "~/lib/logger"; import type { ActionFunctionArgs } from "react-router"; export async function action(args: ActionFunctionArgs) { @@ -208,7 +209,7 @@ export async function action(args: ActionFunctionArgs) { currentPickNumber: nextPickNumber, }); } catch (error) { - console.error("Socket.IO timer-update error:", error); + logger.error("Socket.IO timer-update error:", error); } // Next team's timer is unchanged — their bank carries forward as-is. @@ -239,7 +240,7 @@ export async function action(args: ActionFunctionArgs) { participantId, }); } catch (error) { - console.error("Socket.IO participant-removed-from-queues error:", error); + logger.error("Socket.IO participant-removed-from-queues error:", error); } // Emit socket event @@ -265,7 +266,7 @@ export async function action(args: ActionFunctionArgs) { io.to(`draft-${seasonId}`).emit("draft-completed"); } } catch (error) { - console.error("Socket.IO error:", error); + logger.error("Socket.IO error:", error); } // Check if next team has autodraft enabled and trigger immediately diff --git a/app/routes/api/draft.make-pick.ts b/app/routes/api/draft.make-pick.ts index 23aa717..62fb94a 100644 --- a/app/routes/api/draft.make-pick.ts +++ b/app/routes/api/draft.make-pick.ts @@ -9,6 +9,7 @@ import { getParticipantsForSeasonWithSports } from "~/models/participant"; import { getSeasonSportsSimple } from "~/models/season-sport"; import { calculatePickInfo, checkAndTriggerNextAutodraft, pruneIneligibleQueueItems } from "~/models/draft-utils"; import { getSocketIO } from "../../../server/socket"; +import { logger } from "~/lib/logger"; import type { ActionFunctionArgs } from "react-router"; export async function action(args: ActionFunctionArgs) { @@ -174,7 +175,7 @@ export async function action(args: ActionFunctionArgs) { participantId, }); } catch (error) { - console.error("Socket.IO participant-removed-from-queues error:", error); + logger.error("Socket.IO participant-removed-from-queues error:", error); } // Proactively prune queue items that are now ineligible due to this pick @@ -194,7 +195,7 @@ export async function action(args: ActionFunctionArgs) { }); } } catch (error) { - console.error("Queue pruning error after pick:", error); + logger.error("Queue pruning error after pick:", error); } // Calculate next pick info (before updating season) @@ -262,7 +263,7 @@ export async function action(args: ActionFunctionArgs) { currentPickNumber: nextPickNumber, }); } catch (error) { - console.error("Socket.IO timer-update error:", error); + logger.error("Socket.IO timer-update error:", error); } // Next team's timer is unchanged — their bank carries forward as-is @@ -297,7 +298,7 @@ export async function action(args: ActionFunctionArgs) { getSocketIO().to(`draft-${seasonId}`).emit("draft-completed"); } } catch (error) { - console.error("Socket.IO error:", error); + logger.error("Socket.IO error:", error); } // Check if next team has autodraft enabled and trigger immediately diff --git a/app/routes/api/draft.pause.ts b/app/routes/api/draft.pause.ts index dfc1771..e523550 100644 --- a/app/routes/api/draft.pause.ts +++ b/app/routes/api/draft.pause.ts @@ -4,6 +4,7 @@ import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { isCommissioner } from "~/models/commissioner"; import { getSocketIO } from "../../../server/socket"; +import { logger } from "~/lib/logger"; import type { ActionFunctionArgs } from "react-router"; export async function action(args: ActionFunctionArgs) { @@ -61,7 +62,7 @@ export async function action(args: ActionFunctionArgs) { paused: true, }); } catch (error) { - console.error("Socket.IO error:", error); + logger.error("Socket.IO error:", error); } return Response.json({ success: true, paused: true }); diff --git a/app/routes/api/draft.replace-pick.ts b/app/routes/api/draft.replace-pick.ts index 41c995c..86bdf0c 100644 --- a/app/routes/api/draft.replace-pick.ts +++ b/app/routes/api/draft.replace-pick.ts @@ -8,6 +8,7 @@ import { getDraftPicksWithSports, getTeamDraftPicksWithSports } from "~/models/d import { getParticipantsForSeasonWithSports } from "~/models/participant"; import { getSeasonSportsSimple } from "~/models/season-sport"; import { getSocketIO } from "../../../server/socket"; +import { logger } from "~/lib/logger"; import type { ActionFunctionArgs } from "react-router"; export async function action(args: ActionFunctionArgs) { @@ -179,7 +180,7 @@ export async function action(args: ActionFunctionArgs) { participantId, }); } catch (error) { - console.error("Socket.IO error:", error); + logger.error("Socket.IO error:", error); } return Response.json({ success: true }); diff --git a/app/routes/api/draft.resume.ts b/app/routes/api/draft.resume.ts index 21700b3..18a68bd 100644 --- a/app/routes/api/draft.resume.ts +++ b/app/routes/api/draft.resume.ts @@ -4,6 +4,7 @@ import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { isCommissioner } from "~/models/commissioner"; import { getSocketIO } from "../../../server/socket"; +import { logger } from "~/lib/logger"; import type { ActionFunctionArgs } from "react-router"; export async function action(args: ActionFunctionArgs) { @@ -61,7 +62,7 @@ export async function action(args: ActionFunctionArgs) { paused: false, }); } catch (error) { - console.error("Socket.IO error:", error); + logger.error("Socket.IO error:", error); } return Response.json({ success: true, paused: false }); diff --git a/app/routes/api/draft.rollback.ts b/app/routes/api/draft.rollback.ts index ef3f7ca..c7367bc 100644 --- a/app/routes/api/draft.rollback.ts +++ b/app/routes/api/draft.rollback.ts @@ -4,6 +4,7 @@ import * as schema from "~/database/schema"; import { eq, and, gte } from "drizzle-orm"; import { isCommissioner } from "~/models/commissioner"; import { getSocketIO } from "../../../server/socket"; +import { logger } from "~/lib/logger"; import type { ActionFunctionArgs } from "react-router"; export async function action(args: ActionFunctionArgs) { @@ -92,7 +93,7 @@ export async function action(args: ActionFunctionArgs) { teamId: rollbackSlot?.teamId, }); } catch (error) { - console.error("Socket.IO error:", error); + logger.error("Socket.IO error:", error); } return Response.json({ success: true, pickNumber }); diff --git a/app/routes/api/draft.start.ts b/app/routes/api/draft.start.ts index 1fb7c07..d095447 100644 --- a/app/routes/api/draft.start.ts +++ b/app/routes/api/draft.start.ts @@ -5,6 +5,7 @@ import { eq } from "drizzle-orm"; import { deleteSeasonTimers, initializeDraftTimers } from "~/models/draft-timer"; import { isCommissioner } from "~/models/commissioner"; import { getSocketIO } from "../../../server/socket"; +import { logger } from "~/lib/logger"; import type { ActionFunctionArgs } from "react-router"; export async function action(args: ActionFunctionArgs) { @@ -83,7 +84,7 @@ export async function action(args: ActionFunctionArgs) { currentPickNumber: 1, }); } catch (error) { - console.error("Socket.IO error:", error); + logger.error("Socket.IO error:", error); } return Response.json({ success: true }); diff --git a/app/routes/api/webhooks/clerk.ts b/app/routes/api/webhooks/clerk.ts index da7adb4..ac9561f 100644 --- a/app/routes/api/webhooks/clerk.ts +++ b/app/routes/api/webhooks/clerk.ts @@ -1,6 +1,7 @@ import { Webhook } from "svix"; import type { Route } from "./+types/clerk"; import { findOrCreateUser, getUserDisplayName } from "~/models/user"; +import { logger } from "~/lib/logger"; export async function action({ request }: Route.ActionArgs) { // Get the webhook secret from environment @@ -36,13 +37,13 @@ export async function action({ request }: Route.ActionArgs) { "svix-signature": svix_signature, }) as typeof evt; } catch (err) { - console.error("Error verifying webhook:", err); + logger.error("Error verifying webhook:", err); return new Response("Error: Verification failed", { status: 400 }); } // Handle the webhook const eventType = evt.type; - console.log(`Webhook received: ${eventType}`); + logger.log(`Webhook received: ${eventType}`); if (eventType === "user.created" || eventType === "user.updated") { const { id, email_addresses, username, first_name, last_name, image_url } = evt.data; @@ -61,7 +62,7 @@ export async function action({ request }: Route.ActionArgs) { lastName: last_name, imageUrl: image_url, }); - console.log( + logger.log( `User created in database: ${user.id} (${getUserDisplayName(user)})` ); } else { @@ -77,12 +78,12 @@ export async function action({ request }: Route.ActionArgs) { lastName: last_name, imageUrl: image_url, }); - console.log( + logger.log( `User updated in database: ${user.id} (${getUserDisplayName(user)})` ); } } catch (error) { - console.error(`Error handling ${eventType}:`, error); + logger.error(`Error handling ${eventType}:`, error); return new Response( `Error: Failed to ${eventType === "user.created" ? "create" : "update"} user`, { diff --git a/app/routes/leagues/$leagueId.draft.$seasonId.tsx b/app/routes/leagues/$leagueId.draft.$seasonId.tsx index fea1292..902df7a 100644 --- a/app/routes/leagues/$leagueId.draft.$seasonId.tsx +++ b/app/routes/leagues/$leagueId.draft.$seasonId.tsx @@ -4,6 +4,7 @@ import { eq, and, asc, desc, inArray } from "drizzle-orm"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { useDraftSocket } from "~/hooks/useDraftSocket"; +import { logger } from "~/lib/logger"; import { useCallback, useEffect, useState, useMemo, useRef } from "react"; import { Button } from "~/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; @@ -185,7 +186,7 @@ export async function loader(args: Route.LoaderArgs) { // Load user's team queue if they have a team const userQueue = userTeam ? await getTeamQueue(userTeam.id).catch((err) => { - console.error("[DraftLoader] Failed to load queue, defaulting to empty:", err); + logger.error("[DraftLoader] Failed to load queue, defaulting to empty:", err); return []; }) : []; diff --git a/app/routes/leagues/$leagueId.settings.tsx b/app/routes/leagues/$leagueId.settings.tsx index aaae51d..89ade43 100644 --- a/app/routes/leagues/$leagueId.settings.tsx +++ b/app/routes/leagues/$leagueId.settings.tsx @@ -3,6 +3,7 @@ import { Form, Link, redirect, useNavigation } from "react-router"; import { getAuth } from "@clerk/react-router/server"; import { format } from "date-fns"; import { CalendarIcon } from "lucide-react"; +import { logger } from "~/lib/logger"; import { findLeagueById, updateLeague, deleteLeague } from "~/models/league"; import { sendStandingsUpdateNotification } from "~/services/discord"; import { @@ -222,7 +223,7 @@ export async function action(args: Route.ActionArgs) { discordWebhookUrl: webhookUrl || null, }); } catch (error) { - console.error("Error updating league:", error); + logger.error("Error updating league:", error); return { error: "Failed to update league. Please try again." }; } } @@ -260,7 +261,7 @@ export async function action(args: Route.ActionArgs) { }); return { testSuccess: true }; } catch (err) { - console.error("Discord test webhook failed:", err); + logger.error("Discord test webhook failed:", err); return { error: "Failed to send test notification. Check your webhook URL." }; } } @@ -358,7 +359,7 @@ export async function action(args: Route.ActionArgs) { await removeTeamOwner(teamId); return { success: true, message: "Owner removed successfully" }; } catch (error) { - console.error("Error removing team owner:", error); + logger.error("Error removing team owner:", error); return { error: "Failed to remove owner. Please try again." }; } } @@ -403,7 +404,7 @@ export async function action(args: Route.ActionArgs) { return { success: true, message: "Owner assigned successfully" }; } catch (error) { - console.error("Error assigning team owner:", error); + logger.error("Error assigning team owner:", error); return { error: "Failed to assign owner. Please try again." }; } } @@ -435,7 +436,7 @@ export async function action(args: Route.ActionArgs) { return { success: true, message: "Draft has been reset successfully. Draft order preserved." }; } catch (error) { - console.error("Error resetting draft:", error); + logger.error("Error resetting draft:", error); return { error: "Failed to reset draft. Please try again." }; } } @@ -588,7 +589,7 @@ export async function action(args: Route.ActionArgs) { return redirect(`/leagues/${leagueId}?updated=true`); } catch (error) { - console.error("Error updating season settings:", error); + logger.error("Error updating season settings:", error); return { error: "Failed to update season settings. Please try again." }; } } @@ -611,7 +612,7 @@ export async function action(args: Route.ActionArgs) { await createCommissioner({ leagueId, userId: userClerkId }); return { success: true, message: "Commissioner added successfully" }; } catch (error) { - console.error("Error adding commissioner:", error); + logger.error("Error adding commissioner:", error); return { error: "Failed to add commissioner. Please try again." }; } } @@ -638,7 +639,7 @@ export async function action(args: Route.ActionArgs) { await removeCommissionerByLeagueAndUser(leagueId, commissionerUserId); return { success: true, message: "Commissioner removed successfully" }; } catch (error) { - console.error("Error removing commissioner:", error); + logger.error("Error removing commissioner:", error); return { error: "Failed to remove commissioner. Please try again." }; } } diff --git a/app/routes/leagues/$leagueId.standings.$seasonId.tsx b/app/routes/leagues/$leagueId.standings.$seasonId.tsx index 242c181..91c1a12 100644 --- a/app/routes/leagues/$leagueId.standings.$seasonId.tsx +++ b/app/routes/leagues/$leagueId.standings.$seasonId.tsx @@ -6,6 +6,7 @@ import * as schema from "~/database/schema"; import { Button } from "~/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { StandingsTable } from "~/components/standings/StandingsTable"; +import { logger } from "~/lib/logger"; import { getSevenDayStandingsChange, getSeasonPointProgression } from "~/models/standings"; import { isSeasonComplete, getSeasonCompletionPercentage } from "~/lib/season-helpers.server"; import { PointProgressionChart } from "~/components/standings/PointProgressionChart"; @@ -113,8 +114,8 @@ export async function loader(args: Route.LoaderArgs) { completionPercentage, }; } catch (error) { - console.error("Error loading standings:", error); - console.error("Error details:", { + logger.error("Error loading standings:", error); + logger.error("Error details:", { message: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); diff --git a/app/routes/leagues/new.tsx b/app/routes/leagues/new.tsx index 3d77e8a..13aa59d 100644 --- a/app/routes/leagues/new.tsx +++ b/app/routes/leagues/new.tsx @@ -3,6 +3,7 @@ import { Form, redirect, Link } from "react-router"; import { getAuth } from "@clerk/react-router/server"; import type { Route } from "./+types/new"; +import { logger } from "~/lib/logger"; import { createLeague, setCurrentSeason } from "~/models/league"; import { createCommissioner } from "~/models/commissioner"; import { createSeason } from "~/models/season"; @@ -193,7 +194,7 @@ export async function action(args: Route.ActionArgs) { // Redirect to league homepage return redirect(`/leagues/${league.id}`); } catch (error) { - console.error("Error creating league:", error); + logger.error("Error creating league:", error); return { error: "Failed to create league. Please try again." }; } } diff --git a/app/routes/test-socket.tsx b/app/routes/test-socket.tsx index e0048bb..f67dc27 100644 --- a/app/routes/test-socket.tsx +++ b/app/routes/test-socket.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from "react"; import { useDraftSocket } from "~/hooks/useDraftSocket"; +import { logger } from "~/lib/logger"; export function meta() { return [{ title: "Socket Test - Brackt" }]; @@ -12,7 +13,7 @@ export default function TestSocket() { useEffect(() => { // Listen for test messages const handleTestMessage = (data: unknown) => { - console.log("Received test message:", data); + logger.log("Received test message:", data); setMessages((prev) => [...prev, JSON.stringify(data)]); }; diff --git a/app/services/bracket-simulator.ts b/app/services/bracket-simulator.ts index 4b605cd..b85804a 100644 --- a/app/services/bracket-simulator.ts +++ b/app/services/bracket-simulator.ts @@ -209,7 +209,7 @@ function countsToDistributions( export async function simulateBracket( teams: TeamForSimulation[], format: BracketFormat = 'nhl-8', - simulations: number = 100000, + simulations = 100000, onProgress?: (current: number, total: number) => void ): Promise { // Validate input @@ -280,7 +280,7 @@ export async function simulateBracket( export function simulateBracketSync( teams: TeamForSimulation[], format: BracketFormat = 'nhl-8', - simulations: number = 100000 + simulations = 100000 ): SimulationResults { // Validate input if (format === 'nhl-8' && teams.length !== 8) { diff --git a/app/services/ev-calculator.ts b/app/services/ev-calculator.ts index fceb08b..1f0aafe 100644 --- a/app/services/ev-calculator.ts +++ b/app/services/ev-calculator.ts @@ -79,7 +79,7 @@ export function calculateEV( */ export function validateProbabilities( probabilities: ProbabilityDistribution, - tolerance: number = 0.01 + tolerance = 0.01 ): boolean { const sum = probabilities.probFirst + diff --git a/app/services/icm-calculator.ts b/app/services/icm-calculator.ts index 0e01faa..32fbf7d 100644 --- a/app/services/icm-calculator.ts +++ b/app/services/icm-calculator.ts @@ -1,3 +1,5 @@ +import { logger } from "~/lib/logger"; + /** * ICM (Independent Chip Model) Calculator - Monte Carlo Simulation * @@ -104,8 +106,8 @@ function simulateTournament( */ export function calculateICM( participants: ParticipantChips[], - scoringPlaces: number = 8, - iterations: number = 100000 + scoringPlaces = 8, + iterations = 100000 ): Map { if (participants.length === 0) { return new Map(); @@ -118,7 +120,7 @@ export function calculateICM( championshipProbability: totalProb > 0 ? p.championshipProbability / totalProb : 1 / participants.length, })); - console.log(`[ICM Simulation] Starting ${iterations.toLocaleString()} simulations for ${normalized.length} participants`); + logger.log(`[ICM Simulation] Starting ${iterations.toLocaleString()} simulations for ${normalized.length} participants`); const startTime = Date.now(); // Initialize counters for each participant @@ -142,7 +144,7 @@ export function calculateICM( const progress = ((iter + 1) / iterations * 100).toFixed(0); const elapsed = Date.now() - startTime; const rate = (iter + 1) / (elapsed / 1000); - console.log(`[ICM Simulation] ${progress}% complete (${(iter + 1).toLocaleString()}/${iterations.toLocaleString()}) - ${rate.toFixed(0)} sims/sec`); + logger.log(`[ICM Simulation] ${progress}% complete (${(iter + 1).toLocaleString()}/${iterations.toLocaleString()}) - ${rate.toFixed(0)} sims/sec`); } } @@ -169,7 +171,7 @@ export function calculateICM( } const elapsed = Date.now() - startTime; - console.log(`[ICM Simulation] Completed in ${(elapsed / 1000).toFixed(1)}s (${(iterations / (elapsed / 1000)).toFixed(0)} sims/sec)`); + logger.log(`[ICM Simulation] Completed in ${(elapsed / 1000).toFixed(1)}s (${(iterations / (elapsed / 1000)).toFixed(0)} sims/sec)`); return results; } @@ -228,7 +230,7 @@ function convertAmericanOddsToProbability(odds: number): number { */ export function calculateICMFromOdds( futuresOdds: Array<{ participantId: string; odds: number }>, - scoringPlaces: number = 8 + scoringPlaces = 8 ): Map { // Convert odds to championship probabilities const participants: ParticipantChips[] = futuresOdds.map(({ participantId, odds }) => ({ diff --git a/app/services/probability-updater.ts b/app/services/probability-updater.ts index ab0c894..1ec048d 100644 --- a/app/services/probability-updater.ts +++ b/app/services/probability-updater.ts @@ -111,7 +111,7 @@ function createFinishedProbabilities(finalPosition: number): number[] { */ export async function updateProbabilitiesAfterResult( sportsSeasonId: string, - recalculateUnfinished: boolean = true + recalculateUnfinished = true ): Promise { const errors: string[] = []; let updated = 0; diff --git a/app/services/simulations/ncaam-simulator.ts b/app/services/simulations/ncaam-simulator.ts index eb35850..65a13ac 100644 --- a/app/services/simulations/ncaam-simulator.ts +++ b/app/services/simulations/ncaam-simulator.ts @@ -47,6 +47,7 @@ import * as schema from "~/database/schema"; import type { BracketRegion } from "~/lib/bracket-templates"; import { buildNCAA68SlotMap, matchIndexForSeedSlot, NCAA_68 } from "~/lib/bracket-templates"; import type { Simulator, SimulationResult } from "./types"; +import { logger } from "~/lib/logger"; // ─── Simulation parameters ──────────────────────────────────────────────────── @@ -500,7 +501,7 @@ export class NCAAMSimulator implements Simulator { if (participantRows.length < participantIds.length) { const foundIds = new Set(participantRows.map((r) => r.id)); const missing = participantIds.filter((id) => !foundIds.has(id)); - console.warn( + logger.warn( `[NCAAMSimulator] ${missing.length} participant ID(s) not found in DB: ${missing.join(", ")}. ` + `They will be treated as average strength (KenPom 0.0).` ); diff --git a/app/services/simulations/ncaaw-simulator.ts b/app/services/simulations/ncaaw-simulator.ts index e0769db..0d9e0a6 100644 --- a/app/services/simulations/ncaaw-simulator.ts +++ b/app/services/simulations/ncaaw-simulator.ts @@ -45,6 +45,7 @@ import * as schema from "~/database/schema"; import type { BracketRegion } from "~/lib/bracket-templates"; import { buildNCAA68SlotMap, matchIndexForSeedSlot, NCAA_68 } from "~/lib/bracket-templates"; import type { Simulator, SimulationResult } from "./types"; +import { logger } from "~/lib/logger"; // ─── Simulation parameters ──────────────────────────────────────────────────── @@ -463,7 +464,7 @@ export class NCAAWSimulator implements Simulator { if (participantRows.length < participantIds.length) { const foundIds = new Set(participantRows.map((r) => r.id)); const missing = participantIds.filter((id) => !foundIds.has(id)); - console.warn( + logger.warn( `[NCAAWSimulator] ${missing.length} participant ID(s) not found in DB: ${missing.join(", ")}. ` + `They will be treated as average strength (Barthag ${BARTHAG_FALLBACK}).` ); diff --git a/app/services/simulations/nhl-simulator.ts b/app/services/simulations/nhl-simulator.ts index 9b042c1..2a55aa6 100644 --- a/app/services/simulations/nhl-simulator.ts +++ b/app/services/simulations/nhl-simulator.ts @@ -54,6 +54,7 @@ import { database } from "~/database/context"; import { eq } from "drizzle-orm"; import * as schema from "~/database/schema"; import type { Simulator, SimulationResult } from "./types"; +import { logger } from "~/lib/logger"; // ─── Simulation parameters ──────────────────────────────────────────────────── @@ -353,7 +354,7 @@ export class NHLSimulator implements Simulator { // These are excluded entirely rather than silently landing in the wildcard pool. const unrecognized = teams.filter((t) => !t.data); if (unrecognized.length > 0) { - console.warn( + logger.warn( `[NHLSimulator] ${unrecognized.length} participant(s) not found in TEAMS_DATA and will be excluded: ` + unrecognized.map((t) => t.name).join(", ") ); diff --git a/server.ts b/server.ts index 797a183..d921101 100644 --- a/server.ts +++ b/server.ts @@ -9,6 +9,7 @@ import { fileURLToPath } from "url"; import { drizzle } from "drizzle-orm/postgres-js"; import { migrate } from "drizzle-orm/postgres-js/migrator"; import postgres from "postgres"; +import { logger } from "./server/logger"; // ESM module resolution helpers const __filename = fileURLToPath(import.meta.url); @@ -31,7 +32,7 @@ async function runMigrations(): Promise { try { const db = drizzle(client); await migrate(db, { migrationsFolder: path.join(PROJECT_ROOT, "drizzle") }); - console.log("Migrations complete"); + logger.log("Migrations complete"); } finally { await client.end(); } @@ -44,7 +45,7 @@ async function createAppServer(): Promise { app.disable("x-powered-by"); if (DEVELOPMENT) { - console.log("Starting development server"); + logger.log("Starting development server"); // Dynamic import Vite only in development const { createServer: createViteServer } = await import("vite"); @@ -67,7 +68,7 @@ async function createAppServer(): Promise { } }); } else { - console.log("Starting production server"); + logger.log("Starting production server"); app.use( "/assets", @@ -94,7 +95,7 @@ async function createAppServer(): Promise { // Start server httpServer.listen(PORT, () => { - console.log(`Server is running on http://localhost:${PORT}`); + logger.log(`Server is running on http://localhost:${PORT}`); }); } @@ -102,6 +103,6 @@ async function createAppServer(): Promise { (DEVELOPMENT ? Promise.resolve() : runMigrations()) .then(createAppServer) .catch((error) => { - console.error("Failed to start server:", error); + logger.error("Failed to start server:", error); process.exit(1); }); diff --git a/server/logger.ts b/server/logger.ts new file mode 100644 index 0000000..7e431fd --- /dev/null +++ b/server/logger.ts @@ -0,0 +1,26 @@ +/** + * Server-side logger. In development, delegates to console. In production, + * silences debug noise but keeps warnings and errors visible in server + * stdout/stderr. Sentry is not initialized in this process — if you need + * server-side Sentry coverage, initialize @sentry/node in server.ts. + */ + +const isDev = process.env.NODE_ENV !== "production"; + +function log(...args: unknown[]): void { + if (isDev) console.log(...args); +} + +function info(...args: unknown[]): void { + if (isDev) console.info(...args); +} + +function warn(...args: unknown[]): void { + console.warn(...args); +} + +function error(...args: unknown[]): void { + console.error(...args); +} + +export const logger = { log, info, warn, error }; diff --git a/server/snapshots.ts b/server/snapshots.ts index cf18a23..360d9a9 100644 --- a/server/snapshots.ts +++ b/server/snapshots.ts @@ -3,6 +3,7 @@ import postgres from "postgres"; import * as schema from "~/database/schema"; import { eq, or } from "drizzle-orm"; import { createDailySnapshot } from "~/models/standings"; +import { logger } from "./logger"; // Create a dedicated database connection for the snapshot system const connectionString = process.env.DATABASE_URL; @@ -21,7 +22,7 @@ const CHECK_INTERVAL = 24 * 60 * 60 * 1000; // Check once per day (in millisecon */ export function startSnapshotSystem(): void { if (snapshotInterval) { - console.log("[Snapshots] Snapshot system already running"); + logger.log("[Snapshots] Snapshot system already running"); return; } @@ -33,11 +34,11 @@ export function startSnapshotSystem(): void { try { await createDailySnapshots(); } catch (error) { - console.error("[Snapshots] Error creating daily snapshots:", error); + logger.error("[Snapshots] Error creating daily snapshots:", error); } }, CHECK_INTERVAL); - console.log("[Snapshots] Daily snapshot system started (runs once per day)"); + logger.log("[Snapshots] Daily snapshot system started (runs once per day)"); } /** @@ -47,7 +48,7 @@ export function stopSnapshotSystem(): void { if (snapshotInterval) { clearInterval(snapshotInterval); snapshotInterval = null; - console.log("[Snapshots] Snapshot system stopped"); + logger.log("[Snapshots] Snapshot system stopped"); } } @@ -59,7 +60,7 @@ async function createDailySnapshots(): Promise { const now = new Date(); const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`; - console.log(`[Snapshots] Checking for snapshots to create (${today})`); + logger.log(`[Snapshots] Checking for snapshots to create (${today})`); // Get all seasons that are active or in draft (we want to track standings for these) const activeSeasons = await db.query.seasons.findMany({ @@ -70,22 +71,22 @@ async function createDailySnapshots(): Promise { }); if (activeSeasons.length === 0) { - console.log("[Snapshots] No active seasons found"); + logger.log("[Snapshots] No active seasons found"); return; } - console.log(`[Snapshots] Found ${activeSeasons.length} active season(s)`); + logger.log(`[Snapshots] Found ${activeSeasons.length} active season(s)`); for (const season of activeSeasons) { try { await createDailySnapshot(season.id, db); - console.log(`[Snapshots] ✅ Upserted snapshot for season ${season.id}`); + logger.log(`[Snapshots] ✅ Upserted snapshot for season ${season.id}`); } catch (error) { - console.error(`[Snapshots] Error creating snapshot for season ${season.id}:`, error); + logger.error(`[Snapshots] Error creating snapshot for season ${season.id}:`, error); } } - console.log("[Snapshots] Daily snapshot check complete"); + logger.log("[Snapshots] Daily snapshot check complete"); } /** @@ -93,14 +94,14 @@ async function createDailySnapshots(): Promise { * Useful for admin tools or manual triggers */ export async function createSnapshotsForSeasons(seasonIds: string[]): Promise { - console.log(`[Snapshots] Manual trigger for ${seasonIds.length} season(s)`); + logger.log(`[Snapshots] Manual trigger for ${seasonIds.length} season(s)`); for (const seasonId of seasonIds) { try { await createDailySnapshot(seasonId, db); - console.log(`[Snapshots] ✅ Created snapshot for season ${seasonId}`); + logger.log(`[Snapshots] ✅ Created snapshot for season ${seasonId}`); } catch (error) { - console.error(`[Snapshots] Error creating snapshot for season ${seasonId}:`, error); + logger.error(`[Snapshots] Error creating snapshot for season ${seasonId}:`, error); throw error; } } @@ -111,6 +112,6 @@ export async function createSnapshotsForSeasons(seasonIds: string[]): Promise { - console.log("[Snapshots] Manual trigger for all active seasons"); + logger.log("[Snapshots] Manual trigger for all active seasons"); await createDailySnapshots(); } diff --git a/server/socket.ts b/server/socket.ts index 9633867..29c31ad 100644 --- a/server/socket.ts +++ b/server/socket.ts @@ -6,6 +6,7 @@ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "~/database/schema"; import { eq, and, asc } from "drizzle-orm"; +import { logger } from "./logger"; // Lazy-initialized DB for socket-level validation queries (team ownership checks) let _socketDb: PostgresJsDatabase | null = null; @@ -100,7 +101,7 @@ const connectedTeams = new Map>(); // seasonId -> Set) => { - console.log("Client connected:", socket.id); + logger.log("Client connected:", socket.id); // Store team ID for this socket let currentTeamId: string | undefined; @@ -131,12 +132,12 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer { socket.on("join-draft", async (seasonId: string, teamId?: string) => { if (!seasonId) { - console.error("No seasonId provided for join-draft"); + logger.error("No seasonId provided for join-draft"); return; } socket.join(`draft-${seasonId}`); currentSeasonId = seasonId; - console.log(`Socket ${socket.id} joined draft-${seasonId}`); + logger.log(`Socket ${socket.id} joined draft-${seasonId}`); // If teamId provided, validate it belongs to this season before tracking if (teamId) { @@ -146,11 +147,11 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer { where: and(eq(schema.teams.id, teamId), eq(schema.teams.seasonId, seasonId)), }); if (!team) { - console.warn(`[Socket] join-draft rejected: team ${teamId} does not belong to season ${seasonId}`); + logger.warn(`[Socket] join-draft rejected: team ${teamId} does not belong to season ${seasonId}`); return; } } catch (err) { - console.error("[Socket] join-draft team validation failed:", err); + logger.error("[Socket] join-draft team validation failed:", err); return; } @@ -174,7 +175,7 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer { // Broadcast to everyone else that this team connected io?.to(`draft-${seasonId}`).emit("team-connected", { teamId }); - console.log(`Team ${teamId} connected to draft-${seasonId}. Total connected: ${seasonConnectedTeams.size}`); + logger.log(`Team ${teamId} connected to draft-${seasonId}. Total connected: ${seasonConnectedTeams.size}`); } // Send full draft state to the joining client so it can sync immediately. @@ -244,7 +245,7 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer { }); } } catch (err) { - console.error("[Socket] draft-state-sync query failed:", err); + logger.error("[Socket] draft-state-sync query failed:", err); // Non-fatal — the client will fall back to HTTP revalidation } }); @@ -252,7 +253,7 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer { socket.on("leave-draft", (seasonId: string) => { if (!seasonId) return; socket.leave(`draft-${seasonId}`); - console.log(`Socket ${socket.id} left draft-${seasonId}`); + logger.log(`Socket ${socket.id} left draft-${seasonId}`); // Emit disconnection event if team was tracked if (currentTeamId) { @@ -262,7 +263,7 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer { const seasonConnectedTeams = connectedTeams.get(seasonId); if (seasonConnectedTeams) { seasonConnectedTeams.delete(currentTeamId); - console.log(`Team ${currentTeamId} removed from tracking. Remaining: ${seasonConnectedTeams.size}`); + logger.log(`Team ${currentTeamId} removed from tracking. Remaining: ${seasonConnectedTeams.size}`); // Clean up empty sets if (seasonConnectedTeams.size === 0) { @@ -271,12 +272,12 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer { } io?.to(`draft-${seasonId}`).emit("team-disconnected", { teamId: currentTeamId }); - console.log(`Team ${currentTeamId} disconnected from draft-${seasonId}`); + logger.log(`Team ${currentTeamId} disconnected from draft-${seasonId}`); } }); socket.on("test-event", (data: unknown) => { - console.log("📨 Received test-event from client:", socket.id, data); + logger.log("📨 Received test-event from client:", socket.id, data); socket.emit("test-message", { originalMessage: data, @@ -285,11 +286,11 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer { socketId: socket.id, }); - console.log("✅ Sent test-message response to client:", socket.id); + logger.log("✅ Sent test-message response to client:", socket.id); }); socket.on("disconnect", () => { - console.log("Client disconnected:", socket.id); + logger.log("Client disconnected:", socket.id); // Emit disconnection event if team was tracked if (currentTeamId && currentSeasonId) { @@ -297,7 +298,7 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer { const seasonConnectedTeams = connectedTeams.get(currentSeasonId); if (seasonConnectedTeams) { seasonConnectedTeams.delete(currentTeamId); - console.log(`Team ${currentTeamId} removed from tracking on disconnect. Remaining: ${seasonConnectedTeams.size}`); + logger.log(`Team ${currentTeamId} removed from tracking on disconnect. Remaining: ${seasonConnectedTeams.size}`); // Clean up empty sets if (seasonConnectedTeams.size === 0) { @@ -306,7 +307,7 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer { } io?.to(`draft-${currentSeasonId}`).emit("team-disconnected", { teamId: currentTeamId }); - console.log(`Team ${currentTeamId} disconnected from draft-${currentSeasonId}`); + logger.log(`Team ${currentTeamId} disconnected from draft-${currentSeasonId}`); } }); }); @@ -314,20 +315,20 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer { // Store globally for route handlers global.__socketIO = io; - console.log("Socket.IO initialized"); + logger.log("Socket.IO initialized"); // Start the draft timer system (async import but don't await) import("./timer").then(({ startDraftTimerSystem }) => { startDraftTimerSystem(); }).catch((error) => { - console.error("Failed to start timer system:", error); + logger.error("Failed to start timer system:", error); }); // Start the daily snapshot system import("./snapshots").then(({ startSnapshotSystem }) => { startSnapshotSystem(); }).catch((error) => { - console.error("Failed to start snapshot system:", error); + logger.error("Failed to start snapshot system:", error); }); return io; diff --git a/server/timer.ts b/server/timer.ts index 8b0007a..4db7df1 100644 --- a/server/timer.ts +++ b/server/timer.ts @@ -5,6 +5,7 @@ import { eq, and, asc, sql } from "drizzle-orm"; import type { InferSelectModel } from "drizzle-orm"; import { getSocketIO } from "./socket"; import { executeAutoPick, calculatePickInfo } from "~/models/draft-utils"; +import { logger } from "./logger"; // Create a dedicated database connection for the timer const connectionString = process.env.DATABASE_URL; @@ -22,7 +23,7 @@ let timerInterval: NodeJS.Timeout | null = null; */ export function startDraftTimerSystem(): void { if (timerInterval) { - console.log("[Timer] Timer system already running"); + logger.log("[Timer] Timer system already running"); return; } @@ -30,11 +31,11 @@ export function startDraftTimerSystem(): void { try { await updateDraftTimers(); } catch (error) { - console.error("[Timer] Error updating draft timers:", error); + logger.error("[Timer] Error updating draft timers:", error); } }, 1000); - console.log("[Timer] Draft timer system started"); + logger.log("[Timer] Draft timer system started"); } /** @@ -44,7 +45,7 @@ export function stopDraftTimerSystem(): void { if (timerInterval) { clearInterval(timerInterval); timerInterval = null; - console.log("[Timer] Draft timer system stopped"); + logger.log("[Timer] Draft timer system stopped"); } } @@ -101,7 +102,7 @@ async function updateDraftTimers(): Promise { }); if (!timer) { - console.warn( + logger.warn( `[Timer] No timer found for team ${currentTeamId} in season ${season.id}, creating with initial time` ); const initialTime = season.draftInitialTime || 120; @@ -127,7 +128,7 @@ async function updateDraftTimers(): Promise { // If timer is at 0 or below, check autodraft settings and trigger auto-pick if (timer.timeRemaining <= 0) { - console.log( + logger.log( `[Timer] ⚠️ Timer expired for team ${currentTeamId} in season ${season.id} (pick ${currentPickNumber})` ); @@ -144,7 +145,7 @@ async function updateDraftTimers(): Promise { const success = await triggerAutoPick(season.id, currentTeamId, currentPickNumber, shouldAutodraft ? (autodraftSettings ?? null) : null); if (!success) { - console.error(`[Timer] Pausing draft ${season.id} — auto-pick failed for team ${currentTeamId} pick ${currentPickNumber}`); + logger.error(`[Timer] Pausing draft ${season.id} — auto-pick failed for team ${currentTeamId} pick ${currentPickNumber}`); await db.update(schema.seasons).set({ draftPaused: true }).where(eq(schema.seasons.id, season.id)); io.to(`draft-${season.id}`).emit("draft-paused", { seasonId: season.id, paused: true }); } @@ -186,7 +187,7 @@ async function updateDraftTimers(): Promise { const success = await triggerAutoPick(season.id, currentTeamId, currentPickNumber, shouldAutodraft ? (autodraftSettings ?? null) : null); if (!success) { - console.error(`[Timer] Pausing draft ${season.id} — auto-pick failed for team ${currentTeamId} pick ${currentPickNumber}`); + logger.error(`[Timer] Pausing draft ${season.id} — auto-pick failed for team ${currentTeamId} pick ${currentPickNumber}`); await db.update(schema.seasons).set({ draftPaused: true }).where(eq(schema.seasons.id, season.id)); io.to(`draft-${season.id}`).emit("draft-paused", { seasonId: season.id, paused: true }); } @@ -220,13 +221,13 @@ async function triggerAutoPick( if (result.error === "Pick already made") { return true; } - console.error(`[Timer] Auto-pick failed: ${result.error}`); + logger.error(`[Timer] Auto-pick failed: ${result.error}`); return false; } return true; } catch (error) { - console.error("[Timer] Error in triggerAutoPick:", error); + logger.error("[Timer] Error in triggerAutoPick:", error); return false; } }