Replace console.* with structured logger, fix no-inferrable-types (closes #98) (#199)

- Add app/lib/logger.ts: dev passes through to console; prod routes errors
  to Sentry.captureException and warnings to Sentry.captureMessage, with
  extra context preserved. Uses captureMessage (not captureException) for
  string-only args to avoid fabricated stack traces.
- Add server/logger.ts: dev passes through; prod silences log/info but
  keeps warn/error on stderr (Sentry not initialized in that process).
- Replace all console.* calls across 44 app files and 4 server files.
- Upgrade no-console from warn → error in oxlint; exempt logger files and
  scripts/** via overrides.
- Add typescript/no-inferrable-types rule; fix violations in services and
  simulators. Exempt test files (intentional string widening for switch/if
  tests would break under literal type inference).

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-03-21 13:41:39 -07:00 committed by GitHub
parent 4bffa40606
commit 618bc57ec1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 336 additions and 200 deletions

View file

@ -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/**"
]
}

View file

@ -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);
}
},
},

View file

@ -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();
};

56
app/lib/logger.ts Normal file
View file

@ -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 };

View file

@ -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",

View file

@ -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 T5T8 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}`
);
}

View file

@ -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}`);
}
/**

View file

@ -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);

View file

@ -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",
};

View file

@ -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" };
}
}

View file

@ -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." };
}
}

View file

@ -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"
};

View file

@ -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',

View file

@ -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." };
}
}

View file

@ -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." };
}
}

View file

@ -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." };
}
}

View file

@ -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." };
}
}

View file

@ -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." };
}
}

View file

@ -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 {

View file

@ -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." };
}
}

View file

@ -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",

View file

@ -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." };
}
}

View file

@ -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." };
}
}

View file

@ -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);
});
}

View file

@ -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 });

View file

@ -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

View file

@ -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

View file

@ -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 });

View file

@ -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 });

View file

@ -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 });

View file

@ -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 });

View file

@ -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 });

View file

@ -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`,
{

View file

@ -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 [];
})
: [];

View file

@ -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." };
}
}

View file

@ -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,
});

View file

@ -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." };
}
}

View file

@ -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)]);
};

View file

@ -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<SimulationResults> {
// 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) {

View file

@ -79,7 +79,7 @@ export function calculateEV(
*/
export function validateProbabilities(
probabilities: ProbabilityDistribution,
tolerance: number = 0.01
tolerance = 0.01
): boolean {
const sum =
probabilities.probFirst +

View file

@ -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<string, ICMResult> {
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<string, ICMResult> {
// Convert odds to championship probabilities
const participants: ParticipantChips[] = futuresOdds.map(({ participantId, odds }) => ({

View file

@ -111,7 +111,7 @@ function createFinishedProbabilities(finalPosition: number): number[] {
*/
export async function updateProbabilitiesAfterResult(
sportsSeasonId: string,
recalculateUnfinished: boolean = true
recalculateUnfinished = true
): Promise<ProbabilityUpdateResult> {
const errors: string[] = [];
let updated = 0;

View file

@ -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).`
);

View file

@ -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}).`
);

View file

@ -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(", ")
);

View file

@ -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<void> {
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<void> {
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<void> {
}
});
} else {
console.log("Starting production server");
logger.log("Starting production server");
app.use(
"/assets",
@ -94,7 +95,7 @@ async function createAppServer(): Promise<void> {
// 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<void> {
(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);
});

26
server/logger.ts Normal file
View file

@ -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 };

View file

@ -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<void> {
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<void> {
});
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<void> {
* Useful for admin tools or manual triggers
*/
export async function createSnapshotsForSeasons(seasonIds: string[]): Promise<void> {
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<vo
* Useful for testing or manual refreshes
*/
export async function createSnapshotsForAllSeasons(): Promise<void> {
console.log("[Snapshots] Manual trigger for all active seasons");
logger.log("[Snapshots] Manual trigger for all active seasons");
await createDailySnapshots();
}

View file

@ -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<typeof schema> | null = null;
@ -100,7 +101,7 @@ const connectedTeams = new Map<string, Set<string>>(); // seasonId -> Set<teamId
*/
export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer {
if (io) {
console.log("Socket.IO already initialized");
logger.log("Socket.IO already initialized");
return io;
}
@ -123,7 +124,7 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer {
// Connection handling
io.on("connection", (socket: Socket<ClientToServerEvents, ServerToClientEvents>) => {
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;

View file

@ -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<void> {
});
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<void> {
// 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<void> {
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<void> {
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;
}
}