* Update claude.md to push more tests. * Archiving old plans. * fix: run DB migrations programmatically on server startup Replace unreliable drizzle-kit CLI migration with drizzle-orm's built-in migrator running in the server process before accepting connections. This ensures migrations are always applied on deploy and fails fast if they error. - Add runMigrations() to server.ts using drizzle-orm/postgres-js/migrator - Skip migrations in development (handled manually via db:migrate) - Add DATABASE_URL guard with a clear error message - Remove start:production script (now identical to start) - Update Dockerfile CMD to use npm run start Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
26 KiB
Draft Room Implementation Plan
✅ IMPLEMENTATION STATUS: 90% COMPLETE
Last Updated: October 18, 2025
Quick Summary
- ✅ Phases 1-7: COMPLETE (Database, Settings, Socket.IO, Draft Room, Grid, Player List, Queue)
- 🟡 Phase 8: PARTIAL (Timer display done, server-side countdown NOT implemented)
- ✅ Phase 9: COMPLETE (Draft actions, commissioner controls, force pick)
- 🟡 Phase 10: PARTIAL (Pick updates work, timer sync not implemented)
- ❌ Phase 11: NOT STARTED (Mobile polish)
What Works Now
- Full draft room with snake draft grid
- Real-time pick updates via Socket.IO
- Team queues with add/remove/clear
- Commissioner force pick (auto + manual)
- Search, filter, and hide drafted toggle
- Start draft functionality
- Timer display (client-side only)
What's Missing
- Server-side timer countdown
- Auto-pick when timer expires
- Pause/Resume draft
- Timer persistence across server restarts
- Mobile responsive tweaks
Overview
Build a real-time draft room using Socket.IO for live updates, with snake draft grid, player selection, per-team queue, and chess clock timers.
Phase 1: Database Schema & Models ✅ COMPLETE
1.1 New Tables ✅ COMPLETE
draft_picks
- id (uuid, primary key)
- season_id (uuid, references seasons)
- team_id (uuid, references teams)
- participant_id (uuid, references participants)
- pick_number (integer) - overall pick number
- round (integer)
- pick_in_round (integer)
- picked_by_user_id (varchar) - Clerk user ID
- picked_by_type (enum: 'owner', 'commissioner', 'auto') - who made the pick
- time_used (integer) - seconds used for this pick
- created_at (timestamp)
draft_queue
- id (uuid, primary key)
- season_id (uuid, references seasons)
- team_id (uuid, references teams)
- participant_id (uuid, references participants)
- queue_position (integer)
- created_at (timestamp)
- updated_at (timestamp)
draft_timers
- id (uuid, primary key)
- season_id (uuid, references seasons)
- team_id (uuid, references teams)
- time_remaining (integer) - seconds remaining
- updated_at (timestamp)
1.2 Schema Updates ✅ COMPLETE
Update seasons table:
- draft_initial_time (integer) - seconds, default 120 (2 minutes)
- draft_increment_time (integer) - seconds, default 30
- current_pick_number (integer) - track current pick
- draft_started_at (timestamp)
- draft_paused (boolean, default false)
1.3 Model Functions 🟡 PARTIAL
✅ Implemented:
getTeamQueue(teamId)- inapp/models/draft-queue.tsaddToQueue(),removeFromQueue(),clearTeamQueue(),reorderQueueWithSeason()- inapp/models/draft-queue.ts- Draft pick creation - in API routes
❌ Not Implemented:
updateTeamTimer(teamId, timeRemaining)- not needed yet (no server-side countdown)getTeamTimers(seasonId)- not needed yet- Standalone model functions (using inline queries in API routes instead)
Phase 2: League Settings Updates ✅ COMPLETE
2.1 Add Timer Configuration to Settings Page ✅ COMPLETE
- Add fields to "Draft Rounds" card:
- Initial Time (seconds) - default 120
- Increment Time (seconds) - default 30
- Update action handler to save these values
- Show in league info panel
Phase 3: Socket.IO Setup ✅ COMPLETE
Overview ✅ COMPLETE
Socket.IO requires access to the HTTP server instance (not just the Express app). The key pattern is:
const server = http.createServer(app);
const io = socketIo(server);
server.listen(port); // NOT app.listen()
Architecture Decision
Option A: Modify server.js (Recommended)
- Change
app.listen()tohttp.createServer(app)+server.listen() - Initialize Socket.IO with the HTTP server
- Keep everything in one process
- ✅ Simple, standard pattern
- ✅ Socket.IO and Express share same port
- ✅ Works with React Router v7 SSR
- ❌ Requires modifying server.js
Option B: Separate Socket Server
- Run Socket.IO on different port (e.g., 3001)
- Keep server.js untouched
- ✅ No changes to existing server
- ❌ CORS complexity
- ❌ Two processes to manage
- ❌ Different ports for HTTP and WebSocket
Decision: Use Option A - Industry standard, simpler deployment, better DX
3.1 Server Setup
Step 1: Modify server.js to use HTTP server
File: server.js
Change from:
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
To:
import { createServer } from "http";
const httpServer = createServer(app);
// Import and initialize Socket.IO
const { initializeSocketIO } = await import("./server/socket.js");
initializeSocketIO(httpServer);
httpServer.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
Step 2: Create Socket.IO module
File: server/socket.js (use .js not .ts to avoid bundling issues)
import { Server as SocketIOServer } from "socket.io";
let io;
export function initializeSocketIO(httpServer) {
if (io) {
console.log("Socket.IO already initialized");
return io;
}
io = new SocketIOServer(httpServer, {
cors: {
origin: process.env.NODE_ENV === "production"
? process.env.APP_URL
: `http://localhost:${process.env.PORT || 3000}`,
credentials: true,
},
});
io.on("connection", (socket) => {
console.log("Client connected:", socket.id);
// Join draft room
socket.on("join-draft", (seasonId) => {
socket.join(`draft-${seasonId}`);
console.log(`Socket ${socket.id} joined draft-${seasonId}`);
});
// Leave draft room
socket.on("leave-draft", (seasonId) => {
socket.leave(`draft-${seasonId}`);
console.log(`Socket ${socket.id} left draft-${seasonId}`);
});
socket.on("disconnect", () => {
console.log("Client disconnected:", socket.id);
});
});
// Store globally for access in route handlers
global.__socketIO = io;
console.log("Socket.IO initialized");
return io;
}
export function getSocketIO() {
if (!io && !global.__socketIO) {
throw new Error("Socket.IO not initialized");
}
return io || global.__socketIO;
}
Step 3: Add global type definition
File: server/types.d.ts
import type { Server as SocketIOServer } from "socket.io";
declare global {
var __socketIO: SocketIOServer | undefined;
}
export {};
Step 4: Update tsconfig.node.json
Add server directory to includes:
{
"include": [
"server.js",
"server/**/*.js",
"server/**/*.ts",
"vite.config.ts"
]
}
3.2 Socket Events (Server)
Listen for:
join-draft- user joins draft roomleave-draft- user leaves draft roommake-pick- user makes a pick (handled via HTTP POST, not socket)add-to-queue- add player to queue (handled via HTTP POST)remove-from-queue- remove from queue (handled via HTTP POST)reorder-queue- reorder queue (handled via HTTP POST)
Note: Write operations (picks, queue changes) should use HTTP POST requests for reliability and error handling. Socket.IO is for broadcasting updates to all clients.
Emit (from server to clients):
pick-made- broadcast new pick to roomtimer-update- broadcast timer updates (every second)current-pick-changed- notify whose turn it isdraft-started- draft has begundraft-paused- draft paused/resumeddraft-completed- all picks madequeue-updated- queue changed (only to team owner)
3.3 Client Hook
File: app/hooks/useDraftSocket.ts
import { useEffect, useRef, useState } from "react";
import { io, Socket } from "socket.io-client";
export function useDraftSocket(seasonId: string) {
const socketRef = useRef<Socket | null>(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
// Connect to Socket.IO server
const socket = io({
path: "/socket.io",
transports: ["websocket", "polling"],
});
socketRef.current = socket;
socket.on("connect", () => {
console.log("Connected to Socket.IO");
setIsConnected(true);
// Join the draft room
socket.emit("join-draft", seasonId);
});
socket.on("disconnect", () => {
console.log("Disconnected from Socket.IO");
setIsConnected(false);
});
// Cleanup on unmount
return () => {
if (socket) {
socket.emit("leave-draft", seasonId);
socket.disconnect();
}
};
}, [seasonId]);
// Helper to subscribe to events
const on = (event: string, callback: (...args: any[]) => void) => {
socketRef.current?.on(event, callback);
};
// Helper to unsubscribe from events
const off = (event: string, callback?: (...args: any[]) => void) => {
socketRef.current?.off(event, callback);
};
return { socket: socketRef.current, isConnected, on, off };
}
3.4 Using Socket in Route Handlers
When a pick is made via HTTP POST, broadcast to all clients:
Example in app/routes/api/draft-pick.ts:
import { getSocketIO } from "~/server/socket.js";
export async function action({ request }: ActionFunctionArgs) {
// ... validate and create pick ...
const pick = await createDraftPick({...});
// Broadcast to all clients in the draft room
const io = getSocketIO();
io.to(`draft-${seasonId}`).emit("pick-made", {
pick,
nextPickNumber,
isDraftComplete,
});
return json({ success: true, pick });
}
3.5 Testing Socket.IO
- Start dev server:
npm run dev - Check console for "Socket.IO initialized"
- Open browser console on draft page
- Should see "Connected to Socket.IO"
- Check server logs for "Client connected: [socket-id]"
3.6 Gotchas & Important Notes
⚠️ Gotcha #1: Import path in route handlers
// ❌ WRONG - This won't work in React Router v7
import { getSocketIO } from "~/server/socket.js";
// ✅ CORRECT - Use relative path or configure alias
import { getSocketIO } from "../../../server/socket.js";
Why: React Router v7 runs server code, but ~ alias typically only works for app code. You may need to configure the import path or use relative imports.
⚠️ Gotcha #2: Client hook creates new connection on every render
The current useDraftSocket hook will reconnect if seasonId changes. This is correct, but be aware:
- Don't call this hook multiple times in the same component tree
- Don't pass a changing
seasonIdunless you want to switch rooms - Consider memoizing the
seasonIdprop
⚠️ Gotcha #3: Event listener cleanup
// ❌ WRONG - Listeners not cleaned up
useEffect(() => {
socket.on("pick-made", handlePickMade);
}, []);
// ✅ CORRECT - Clean up listeners
useEffect(() => {
socket.on("pick-made", handlePickMade);
return () => {
socket.off("pick-made", handlePickMade);
};
}, [handlePickMade]);
⚠️ Gotcha #4: Socket.IO path in production
If you deploy behind a reverse proxy (nginx, etc.), you may need to configure the Socket.IO path:
// Client
const socket = io({
path: "/socket.io", // Make sure this matches your proxy config
});
// Server (if needed)
const io = new SocketIOServer(httpServer, {
path: "/socket.io",
});
⚠️ Gotcha #5: CORS might not be needed
Since Socket.IO runs on the same server/port as your app, CORS might be unnecessary for same-origin requests. You can simplify:
// Simpler config if same-origin
const io = new SocketIOServer(httpServer);
⚠️ Gotcha #6: Global storage in development
The global __socketIO pattern works, but in development with HMR, you might get multiple instances. The if (io) check helps, but be aware of potential stale connections during hot reloads.
⚠️ Gotcha #7: TypeScript import in route handlers
// This might cause issues because socket.js is JavaScript
import { getSocketIO } from "~/server/socket.js";
// Consider making it TypeScript with proper types
// Or use @ts-ignore if necessary
// @ts-ignore
import { getSocketIO } from "~/server/socket.js";
3.7 Production Considerations
- Socket.IO will automatically upgrade from polling to WebSocket
- Ensure load balancer supports WebSocket (sticky sessions required!)
- Consider Redis adapter for multi-server deployments (future)
- Monitor connection counts and memory usage
- Set up proper error handling for disconnections
- Consider implementing reconnection logic with exponential backoff
Phase 4: Draft Room Route & Layout ✅ COMPLETE
4.1 Route Setup ✅ COMPLETE
- Create
/app/routes/leagues/$leagueId.draft.tsx - Loader:
- Verify user access (team owner or commissioner)
- Load season, teams, draft slots, participants
- Load existing draft picks
- Load user's team queue
- Load timer states
- No action needed (all via Socket.IO)
4.2 Layout Structure
┌─────────────────────────────────────────────────┐
│ Draft Room Header │
│ League Name | Status | Commissioner Controls │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ Draft Grid (horizontally scrollable) │
│ Team 1 Team 2 Team 3 ... │
│ 5:23 4:12 3:45 ... (timers) │
│ ┌────┐ ┌────┐ ┌────┐ │
│ │1.01│ │1.02│ │1.03│ ... │
│ └────┘ └────┘ └────┘ │
│ ┌────┐ ┌────┐ ┌────┐ │
│ │2.03│ │2.02│ │2.01│ ... (snake) │
│ └────┘ └────┘ └────┘ │
└─────────────────────────────────────────────────┘
┌──────────────────────┬──────────────────────────┐
│ Available Players │ My Queue │
│ [Search: ____] │ 1. Player A │
│ [x] Hide Drafted │ 2. Player B │
│ │ 3. Player C │
│ □ Player 1 - NFL │ [Clear Queue] │
│ EV: 125 │ │
│ □ Player 2 - NBA │ │
│ EV: 118 │ │
│ ⊠ Player 3 - MLB │ │
│ (DRAFTED) │ │
└──────────────────────┴──────────────────────────┘
4.3 No Navigation Bar
- Create standalone layout without main nav
- Add "Exit Draft Room" button to return to league page
Phase 5: Draft Grid Component ✅ COMPLETE
5.1 Calculate Snake Order ✅ COMPLETE
- Use existing draft slots for team order
- Generate pick order:
- Round 1: slots in order (1→N)
- Round 2: slots reversed (N→1)
- Round 3: slots in order (1→N)
- etc.
5.2 Grid Display
- Show team names as column headers
- Show timer under each team name
- Render grid cells with pick numbers (1.01, 1.02, etc.)
- Highlight current pick (border, background color)
- Show drafted participant in completed picks
- Responsive: horizontal scroll on smaller screens
5.3 Pick Cell Component
<PickCell
pickNumber="1.01"
round={1}
pickInRound={1}
teamId={teamId}
participant={draftedParticipant}
isCurrent={isCurrentPick}
isUserTeam={isUserTeam}
/>
Phase 6: Player List Component ✅ COMPLETE
6.1 Player List Features ✅ COMPLETE
- Display all participants from season sports
- Show: "Participant Name - Sport"
- Show expected value
- Sort by: EV (desc), then alphabetical
- Search/filter by name
- Toggle to hide/show drafted players
- Click to add to queue (if not drafted)
- Visual indicator for drafted players
6.2 Player Item Component
<PlayerItem
participant={participant}
isDrafted={isDrafted}
onAddToQueue={handleAddToQueue}
/>
Phase 7: Queue Component ✅ COMPLETE
7.1 Queue Features 🟡 MOSTLY COMPLETE
✅ Implemented:
- Show only current user's team queue
- Display in order (1, 2, 3, ...)
- Remove button for each item
- "Clear Queue" button
- Queue icon buttons next to Pick button
❌ Not Implemented:
- Drag & drop to reorder (API route exists, UI not implemented)
- Real-time Socket.IO sync (using HTTP requests + page state instead)
7.2 Queue Actions ✅ COMPLETE
✅ Implemented via API routes:
/api/queue/add- Add player to queue/api/queue/remove- Remove from queue/api/queue/clear- Clear entire queue/api/queue/reorder- Reorder queue (API ready, UI not implemented)
Phase 8: Timer System 🟡 PARTIAL (50% Complete)
8.1 Timer Display ✅ COMPLETE
- Show under each team name in grid
- Format:
- "M:SS" for times under 1 hour (e.g., "5:23")
- "H:MM:SS" for times 1 hour or more (e.g., "2:15:30")
- Color coding:
- Green: > 60s
- Yellow: 30-60s
- Red: < 30s
- Flashing red: < 10s
8.2 Timer Logic (Server-Side) ❌ NOT IMPLEMENTED
What's Missing:
- Server-side countdown interval
- Emit
timer-updateevery second - Auto-pick when timer reaches 0
- Timer increment after pick
- Timer persistence
Note: Timer display shows --:-- because no server-side countdown is running. This is the main missing piece for a fully functional draft.
8.3 Auto-Pick Function 🟡 PARTIAL
✅ Implemented:
/api/draft/force-autopick- Commissioner can force auto-pick- Checks queue first, then picks highest EV
- Creates pick with
picked_by_type='auto'
❌ Not Implemented:
- Automatic trigger when timer reaches 0
- Server-side timer countdown to trigger it
Phase 9: Draft Actions & Permissions ✅ COMPLETE
9.1 Making a Pick ✅ COMPLETE
- Only current team owner or commissioner can pick
- Click participant in player list (if current turn)
- Validate: participant not already drafted
- Create draft pick record
- Emit
pick-madeto room - Update current pick
- Update timers
9.2 Commissioner Controls 🟡 MOSTLY COMPLETE
✅ Implemented:
- Start Draft button (shows when status is "pre_draft")
- Force Auto Pick (right-click context menu on current pick)
- Force Manual Pick (right-click context menu, opens participant dialog)
/api/draft/start- Starts draft, initializes timers/api/draft/make-pick- Regular pick by team owner/api/draft/force-autopick- Commissioner force auto-pick/api/draft/force-manual-pick- Commissioner manual pick for any team
❌ Not Implemented:
- Pause/Resume button
- Pause/Resume API routes
9.3 Draft Start Logic ✅ COMPLETE
✅ Implemented:
- Manual start by commissioner via "Start Draft" button
- Changes season status to "draft"
- Initializes timers for all teams
- Emits
draft-startedsocket event
❌ Not Implemented:
- Auto-start when
draftDateTimeis reached (would need background job/cron)
9.4 Draft Completion ✅ COMPLETE
✅ Implemented:
- Detects when all picks made
- Changes season status to "active"
- Emits
draft-completedsocket event
Phase 10: Real-Time Updates 🟡 PARTIAL (60% Complete)
10.1 Client State Management ✅ COMPLETE
- Use React state for:
- Draft picks array
- Current pick number
- Team timers
- User's queue
- Available participants
- Update state when Socket.IO events received
10.2 Optimistic Updates 🟡 PARTIAL
✅ Implemented:
- Pick updates broadcast via Socket.IO
pick-madeevent - All clients receive and display picks in real-time
- Queue operations update local state immediately
❌ Not Implemented:
- Timer updates via Socket.IO (no server-side countdown)
- Revert on server rejection (currently just shows alert)
Note: Real-time updates work for picks but not for timers since server-side countdown isn't implemented.
Phase 11: Mobile Responsiveness ❌ NOT STARTED
11.1 Mobile Layout Adjustments ❌ NOT STARTED
What's Needed:
- Draft grid: horizontal scroll (partially works)
- Stack player list and queue vertically on mobile
- Collapsible sections for player list/queue
- Larger touch targets for buttons
- Simplified timer display
- Test on actual mobile devices
- Responsive breakpoints optimization
Current State: Desktop layout works, mobile usable but not optimized.
Implementation Order
- Phase 1: Database schema & migrations
- Phase 2: League settings timer config
- Phase 3: Socket.IO setup (server + client hook)
- Phase 4: Draft room route & basic layout
- Phase 5: Draft grid component (static first)
- Phase 6: Player list component
- Phase 7: Queue component
- Phase 8: Timer system (server-side logic)
- Phase 9: Draft actions & permissions
- Phase 10: Wire up real-time updates
- Phase 11: Mobile polish
✅ IMPLEMENTATION COMPLETION SUMMARY
Files Created/Modified
API Routes:
- ✅
/app/routes/api/draft.make-pick.ts- Team owner makes pick - ✅
/app/routes/api/draft.start.ts- Commissioner starts draft - ✅
/app/routes/api/draft.force-autopick.ts- Commissioner force auto-pick - ✅
/app/routes/api/draft.force-manual-pick.ts- Commissioner manual pick - ✅
/app/routes/api/queue.add.ts- Add to queue - ✅
/app/routes/api/queue.remove.ts- Remove from queue - ✅
/app/routes/api/queue.clear.ts- Clear queue - ✅
/app/routes/api/queue.reorder.ts- Reorder queue (API ready, UI not implemented)
Models:
- ✅
/app/models/draft-queue.ts- Queue management functions
Routes:
- ✅
/app/routes/leagues/$leagueId.draft.$seasonId.tsx- Main draft room
Hooks:
- ✅
/app/hooks/useDraftSocket.ts- Socket.IO client hook
Server:
- ✅
/server/socket.js- Socket.IO server setup - ✅
server.js- Modified to use HTTP server for Socket.IO
Components:
- ✅
/app/components/ui/context-menu.tsx- Added for commissioner controls
Database:
- ✅ Schema updated with draft tables and timer fields
- ✅
draftQueueRelationsadded to schema
What Works Right Now
-
Full Draft Flow:
- Commissioner can start draft
- Team owners can make picks on their turn
- Commissioner can force picks (auto or manual)
- Real-time pick updates via Socket.IO
- Draft completion detection
-
Queue Management:
- Add/remove/clear queue
- Queue persists in database
- Queue icon buttons next to Pick button
- Queue used by force autopick
-
UI Features:
- Snake draft grid with pick numbers
- Search participants by name
- Filter by sport
- Hide/show drafted toggle
- Context menu for commissioner force pick
- Responsive grid (horizontal scroll)
-
Real-Time:
- Socket.IO connection indicator
- Pick updates broadcast to all users
- Room-based isolation (multiple drafts can run simultaneously)
What's Missing (10%)
-
Server-Side Timer Countdown (Main Missing Piece):
- No interval running on server
- No
timer-updatesocket events - Timer display shows
--:-- - No automatic trigger for auto-pick
-
Pause/Resume:
- No pause/resume buttons
- No API routes for pause/resume
-
Minor Features:
- Drag & drop queue reorder UI
- Mobile optimization
- Auto-start at
draftDateTime
To Complete the Draft Room (Remaining 10%)
Priority 1: Server-Side Timer (Critical)
- Add interval in
server/socket.jsto countdown every second - Emit
timer-updateto draft room - Trigger auto-pick when timer reaches 0
- Add timer increment after pick
Priority 2: Pause/Resume (Nice to Have)
- Add pause/resume buttons for commissioner
- Create
/api/draft/pauseand/api/draft/resumeroutes - Update timer logic to respect pause state
Priority 3: Polish (Optional)
- Drag & drop queue reorder UI
- Mobile responsive improvements
- Auto-start background job
Open Questions / Future Considerations
- Background job for auto-start: How to trigger draft start at
draftDateTime? Cron job? Polling? - Timer persistence: What if server restarts during draft? Store timer state in DB and resume?
- Undo picks: Not in initial scope, but may want later
- Draft history/log: Separate view to see all picks in order?
- Notifications: Alert users when it's their turn? (email, push, etc.)
- Multi-server: If scaling to multiple servers, need Redis adapter for Socket.IO
Requirements Summary
User Requirements
- Route:
/leagues/$leagueId/draft - Queue: Per-team, private (stored in database)
- Timer: Chess clock (initial time + increment per pick)
- Real-time: WebSocket (Socket.IO)
- Mobile: Should work on mobile
- Commissioner: Can force picks
- Participant sorting: By EV (desc), then alphabetical
- Search: Filter participants by name
- Toggle: Show/hide drafted participants
- Teams: 6-16 teams typical (12 average)
- Timer config: Configurable per league/season
- Auto-pick: From queue, or top EV if queue empty
- Draft state: "drafting" status
- Start: Auto-start at draft date/time, or manual by commissioner
- Pause/Resume: Commissioner can pause/resume
- Pick log: Track who made each pick (owner/commissioner/auto)
- Grid: Horizontally scrollable
- Player display: "Player Name - Sport"
- Pre-draft: Room viewable before draft starts
- Post-draft: Change status to "active" when complete