brackt/plans/completed/draft-room-implementation.md
Chris Parsons 35fe84a1dd
Update claude.md to push more tests. (#84)
* 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>
2026-03-07 22:31:04 -08:00

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) - in app/models/draft-queue.ts
  • addToQueue(), removeFromQueue(), clearTeamQueue(), reorderQueueWithSeason() - in app/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() to http.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 room
  • leave-draft - user leaves draft room
  • make-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 room
  • timer-update - broadcast timer updates (every second)
  • current-pick-changed - notify whose turn it is
  • draft-started - draft has begun
  • draft-paused - draft paused/resumed
  • draft-completed - all picks made
  • queue-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

  1. Start dev server: npm run dev
  2. Check console for "Socket.IO initialized"
  3. Open browser console on draft page
  4. Should see "Connected to Socket.IO"
  5. 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 seasonId unless you want to switch rooms
  • Consider memoizing the seasonId prop

⚠️ 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-update every 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-made to 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-started socket event

Not Implemented:

  • Auto-start when draftDateTime is reached (would need background job/cron)

9.4 Draft Completion COMPLETE

Implemented:

  • Detects when all picks made
  • Changes season status to "active"
  • Emits draft-completed socket 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-made event
  • 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

  1. Phase 1: Database schema & migrations
  2. Phase 2: League settings timer config
  3. Phase 3: Socket.IO setup (server + client hook)
  4. Phase 4: Draft room route & basic layout
  5. Phase 5: Draft grid component (static first)
  6. Phase 6: Player list component
  7. Phase 7: Queue component
  8. Phase 8: Timer system (server-side logic)
  9. Phase 9: Draft actions & permissions
  10. Phase 10: Wire up real-time updates
  11. 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
  • draftQueueRelations added to schema

What Works Right Now

  1. 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
  2. Queue Management:

    • Add/remove/clear queue
    • Queue persists in database
    • Queue icon buttons next to Pick button
    • Queue used by force autopick
  3. 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)
  4. Real-Time:

    • Socket.IO connection indicator
    • Pick updates broadcast to all users
    • Room-based isolation (multiple drafts can run simultaneously)

What's Missing (10%)

  1. Server-Side Timer Countdown (Main Missing Piece):

    • No interval running on server
    • No timer-update socket events
    • Timer display shows --:--
    • No automatic trigger for auto-pick
  2. Pause/Resume:

    • No pause/resume buttons
    • No API routes for pause/resume
  3. 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)

  1. Add interval in server/socket.js to countdown every second
  2. Emit timer-update to draft room
  3. Trigger auto-pick when timer reaches 0
  4. Add timer increment after pick

Priority 2: Pause/Resume (Nice to Have)

  1. Add pause/resume buttons for commissioner
  2. Create /api/draft/pause and /api/draft/resume routes
  3. Update timer logic to respect pause state

Priority 3: Polish (Optional)

  1. Drag & drop queue reorder UI
  2. Mobile responsive improvements
  3. 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