brackt/server/snapshots.ts

109 lines
3.2 KiB
TypeScript
Raw Normal View History

import * as schema from "~/database/schema";
Add oxlint linting setup with zero errors (#194) * Add oxlint and fix all lint errors - Install oxlint, add .oxlintrc.json with rules for TypeScript/React - Add npm run lint / lint:fix scripts - Add Claude PostToolUse hook to run oxlint on every edited file - Fix 101 errors: unused vars/imports, eqeqeq, prefer-const, no-new-array - Fix no-array-index-key (use stable keys or suppress positional cases) - Fix exhaustive-deps missing dependency in useEffect - Promote exhaustive-deps and no-array-index-key to errors - Fix Map.get() !== null bug in $leagueId.server.ts (should be !== undefined) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix no-explicit-any warnings and upgrade tsconfig to ES2023 - Replace all `any` types with proper types or `unknown` across ~20 files - Add typed socket payload interfaces in draft route and useDraftSocket - Use any[] with eslint-disable for socket.io callbacks (legitimate escape hatch) - Bump all tsconfigs from ES2022 → ES2023 to support toSorted/toReversed - Fix cascading type errors uncovered by removing any: Map.get narrowing, participant relation types, ChartDataPoint, Partial<NewSeason> indexing - Add ParticipantResultWithParticipant type to participant-result model - Fix test fixtures to match updated interfaces (DraftCell, ParticipantResult) - Fix duplicate getQPStandings import in sportsSeasonId.server.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Promote no-explicit-any to error Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:44:05 -07:00
import { eq, or } from "drizzle-orm";
import { createDailySnapshot } from "~/models/standings";
import { logger } from "./logger";
Add CS2 Major Qualifying Points simulator and stage management (#260) * Add CS2 Major qualifying points simulator Implements a full CS2 Major tournament simulator with: - 3-stage Swiss format (Opening Bo1, Elimination Bo1/Bo3, Decider all Bo3) + Champions Stage 8-team single-elimination (QF Bo3, SF Bo3, GF Bo5) - Monte Carlo simulation (10,000 iterations) accumulating QP across 2 majors/season - Sampled 24-team field per iteration: top 12 guaranteed, remaining weighted by 1/rank - Stage 3 exits (placements 9-16) sub-ranked by W-L record (2-3 > 1-3 > 0-3) - Stage assignments stored per-event so actual field composition drives simulation - Admin CS Elo form for entering team Elo + HLTV world rankings - Admin CS2 stage setup page for assigning teams to stages and tracking advancement - Database migration: cs2_major_qualifying_points enum value + cs2_major_stage_results table - 24 unit tests covering all exported pure functions https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR * Consolidate Elo + ranking input into generic elo-ratings page The darts-elo and cs-elo pages were unreachable from the admin nav, which always links to the generic elo-ratings page. Extended elo-ratings to conditionally show world ranking fields for simulator types that need it (darts_bracket, cs2_major_qualifying_points), then deleted the redundant sport-specific pages. https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR * Consolidate server postgres connections into one shared pool Four separate postgres() clients were open simultaneously (app, timer, snapshots, socket), each defaulting to 10 connections, exhausting the database's max_connections limit. Replaced with a single shared lazy- initialized client in server/db.ts using a Proxy to defer the DATABASE_URL check until first use (preserving test compatibility). Also bumps the CS2 Champions Stage stochastic test from 200 → 1000 iterations to eliminate flakiness. https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR * Fix and() bug and add Swiss loop safety guard - cs2-major-stage.ts: markCs2StageEliminations and setCs2FinalPlacements were using JS && instead of Drizzle and(), causing WHERE to filter only by participantId (not scoringEventId), which would update rows across all events instead of just the target event - cs-major-simulator.ts: add break guard in simulateSwiss while loop to prevent infinite loop if pairGroups returns no pairs https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR * Fix all remaining code review issues - cs2-major-stage.ts: use schema column reference for stageEliminated in markCs2StageEliminations instead of raw SQL string - cs-major-simulator.ts: simulateOneMajor now locks in known stage results when a stage is complete (8 recorded eliminations), only simulating the remaining stages during live events - admin event page: add CS2 Stage Setup button for cs2_major_qualifying_points simulator types; expose simulatorType in server loader type cast - cs2-setup.tsx: replace document.getElementById DOM manipulation with React state (eliminatedChecked map) for checkbox show/hide logic; remove unused stageMap and unassignedParticipants variables https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR * Fix oxlint errors: non-null assertions, sort→toSorted, unused vars - cs-major-simulator.ts: replace 5 non-null assertions (!) with safe optional chaining / if-guards; replace 6 .sort() with .toSorted() - cs2-major-stage.ts: remove unused `inArray` import - cs2-setup.tsx: remove unused `assignedIds` variable https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR * Fix flaky Champions Stage stochastic test The makeTeams(8) helper creates only a 70-pt Elo spread (1800→1730). With the Champions Stage bracket math this gives team-0 a ~19.6% win rate — right at the 0.2 threshold, causing the test to fail ~63% of the time in CI despite 1000 iterations. Use 100-pt steps (1800→1100) instead, giving team-0 a ~40% win rate and raising the assertion threshold to 0.25 for a clear safety margin. https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-04-05 13:40:05 -07:00
import { db } from "./db";
let snapshotInterval: NodeJS.Timeout | null = null;
const CHECK_INTERVAL = 24 * 60 * 60 * 1000; // Check once per day (in milliseconds)
/**
* Start the daily snapshot system
* Runs once per day to create snapshots for all active seasons
*/
export function startSnapshotSystem(): void {
if (snapshotInterval) {
logger.log("[Snapshots] Snapshot system already running");
return;
}
// Run immediately on startup
void createDailySnapshots();
// Then run once per day
snapshotInterval = setInterval(async () => {
try {
await createDailySnapshots();
} catch (error) {
logger.error("[Snapshots] Error creating daily snapshots:", error);
}
}, CHECK_INTERVAL);
logger.log("[Snapshots] Daily snapshot system started (runs once per day)");
}
/**
* Stop the snapshot system
*/
export function stopSnapshotSystem(): void {
if (snapshotInterval) {
clearInterval(snapshotInterval);
snapshotInterval = null;
logger.log("[Snapshots] Snapshot system stopped");
}
}
/**
* Create daily snapshots for all active seasons
* Only creates snapshots if they don't already exist for today
*/
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")}`;
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({
where: or(
eq(schema.seasons.status, "active"),
eq(schema.seasons.status, "draft")
),
});
if (activeSeasons.length === 0) {
logger.log("[Snapshots] No active seasons found");
return;
}
logger.log(`[Snapshots] Found ${activeSeasons.length} active season(s)`);
for (const season of activeSeasons) {
try {
await createDailySnapshot(season.id, db);
logger.log(`[Snapshots] ✅ Upserted snapshot for season ${season.id}`);
} catch (error) {
logger.error(`[Snapshots] Error creating snapshot for season ${season.id}:`, error);
}
}
logger.log("[Snapshots] Daily snapshot check complete");
}
/**
* Manually trigger snapshot creation for specific seasons
* Useful for admin tools or manual triggers
*/
export async function createSnapshotsForSeasons(seasonIds: string[]): Promise<void> {
logger.log(`[Snapshots] Manual trigger for ${seasonIds.length} season(s)`);
for (const seasonId of seasonIds) {
try {
await createDailySnapshot(seasonId, db);
logger.log(`[Snapshots] ✅ Created snapshot for season ${seasonId}`);
} catch (error) {
logger.error(`[Snapshots] Error creating snapshot for season ${seasonId}:`, error);
throw error;
}
}
}
/**
* Manually trigger snapshot creation for all active seasons
* Useful for testing or manual refreshes
*/
export async function createSnapshotsForAllSeasons(): Promise<void> {
logger.log("[Snapshots] Manual trigger for all active seasons");
await createDailySnapshots();
}