CLAUDE: Add schedule_game_id linking for webapp games to external schedules

Enables precise tracking of which webapp games correspond to specific
scheduled matchups from SBA/PD league systems.

Backend:
- Add schedule_game_id column to games table with index
- Create Alembic migration for the new column
- Update QuickCreateRequest to accept schedule_game_id
- Update GameListItem response to include schedule_game_id
- Update DatabaseOperations.create_game() to store the link

Frontend:
- Pass schedule_game_id when creating game from "Play" button
- Add activeScheduleGameMap to track webapp games by schedule ID
- Show "In Progress" (green) link for active games
- Show "Resume" (green) link for pending games
- Show "Play" (blue) button for unstarted games

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-01-14 23:55:53 -06:00
parent fbbb1cc5da
commit a22513b053
6 changed files with 92 additions and 8 deletions

View File

@ -0,0 +1,37 @@
"""add schedule_game_id to games
Revision ID: 62bd3195c64c
Revises: 005
Create Date: 2026-01-14 23:44:40.038088
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '62bd3195c64c'
down_revision: Union[str, None] = '005'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Add schedule_game_id column for linking webapp games to external schedule systems
op.add_column(
'games',
sa.Column('schedule_game_id', sa.Integer(), nullable=True)
)
# Add index for efficient lookups
op.create_index(
'ix_games_schedule_game_id',
'games',
['schedule_game_id']
)
def downgrade() -> None:
op.drop_index('ix_games_schedule_game_id', table_name='games')
op.drop_column('games', 'schedule_game_id')

View File

@ -33,6 +33,8 @@ class GameListItem(BaseModel):
away_score: int = 0 away_score: int = 0
inning: int | None = None inning: int | None = None
half: str | None = None # 'top' or 'bottom' half: str | None = None # 'top' or 'bottom'
# External schedule reference (for linking to SBA/PD schedule systems)
schedule_game_id: int | None = None
class CreateGameRequest(BaseModel): class CreateGameRequest(BaseModel):
@ -59,6 +61,7 @@ class QuickCreateRequest(BaseModel):
home_team_id: int | None = Field(None, description="Home team ID (uses default if not provided)") home_team_id: int | None = Field(None, description="Home team ID (uses default if not provided)")
away_team_id: int | None = Field(None, description="Away team ID (uses default if not provided)") away_team_id: int | None = Field(None, description="Away team ID (uses default if not provided)")
schedule_game_id: int | None = Field(None, description="External schedule game ID for linking")
class LineupPlayerRequest(BaseModel): class LineupPlayerRequest(BaseModel):
@ -219,6 +222,7 @@ async def list_games():
away_score=game.away_score or 0, away_score=game.away_score or 0,
inning=game.current_inning, inning=game.current_inning,
half=game.current_half, half=game.current_half,
schedule_game_id=game.schedule_game_id,
) )
) )
@ -358,7 +362,7 @@ async def quick_create_game(
game_id = uuid4() game_id = uuid4()
league_id = "sba" league_id = "sba"
# Determine team IDs # Determine team IDs and schedule link
use_custom_teams = ( use_custom_teams = (
request is not None request is not None
and request.home_team_id is not None and request.home_team_id is not None
@ -368,10 +372,12 @@ async def quick_create_game(
if use_custom_teams: if use_custom_teams:
home_team_id = request.home_team_id home_team_id = request.home_team_id
away_team_id = request.away_team_id away_team_id = request.away_team_id
schedule_game_id = request.schedule_game_id
else: else:
# Default demo teams # Default demo teams
home_team_id = 35 home_team_id = 35
away_team_id = 38 away_team_id = 38
schedule_game_id = None
# Get creator's discord_id from authenticated user # Get creator's discord_id from authenticated user
creator_discord_id = user.get("discord_id") if user else None creator_discord_id = user.get("discord_id") if user else None
@ -399,6 +405,7 @@ async def quick_create_game(
away_team_id=away_team_id, away_team_id=away_team_id,
game_mode="friendly", game_mode="friendly",
visibility="public", visibility="public",
schedule_game_id=schedule_game_id,
) )
if use_custom_teams: if use_custom_teams:

View File

@ -102,6 +102,7 @@ class DatabaseOperations:
home_team_is_ai: bool = False, home_team_is_ai: bool = False,
away_team_is_ai: bool = False, away_team_is_ai: bool = False,
ai_difficulty: str | None = None, ai_difficulty: str | None = None,
schedule_game_id: int | None = None,
) -> Game: ) -> Game:
""" """
Create new game in database. Create new game in database.
@ -116,6 +117,7 @@ class DatabaseOperations:
home_team_is_ai: Whether home team is AI home_team_is_ai: Whether home team is AI
away_team_is_ai: Whether away team is AI away_team_is_ai: Whether away team is AI
ai_difficulty: AI difficulty if applicable ai_difficulty: AI difficulty if applicable
schedule_game_id: External schedule game ID for linking (SBA, PD, etc.)
Returns: Returns:
Created Game model Created Game model
@ -134,6 +136,7 @@ class DatabaseOperations:
home_team_is_ai=home_team_is_ai, home_team_is_ai=home_team_is_ai,
away_team_is_ai=away_team_is_ai, away_team_is_ai=away_team_is_ai,
ai_difficulty=ai_difficulty, ai_difficulty=ai_difficulty,
schedule_game_id=schedule_game_id,
status="pending", status="pending",
) )
session.add(game) session.add(game)

View File

@ -99,6 +99,9 @@ class Game(Base):
away_team_is_ai = Column(Boolean, default=False) away_team_is_ai = Column(Boolean, default=False)
ai_difficulty = Column(String(20), nullable=True) ai_difficulty = Column(String(20), nullable=True)
# External schedule reference (league-agnostic - works for SBA, PD, etc.)
schedule_game_id = Column(Integer, nullable=True, index=True)
# Timestamps # Timestamps
created_at = Column( created_at = Column(
DateTime, default=lambda: pendulum.now("UTC").naive(), index=True DateTime, default=lambda: pendulum.now("UTC").naive(), index=True

View File

@ -144,11 +144,21 @@
Final Final
</span> </span>
</template> </template>
<!-- Active webapp game: show "In Progress" link -->
<template v-else-if="activeScheduleGameMap.get(game.id)">
<span class="flex-1"></span>
<NuxtLink
:to="`/games/${activeScheduleGameMap.get(game.id)!.game_id}`"
class="px-2 py-1 text-xs bg-green-600 hover:bg-green-700 text-white font-medium rounded transition"
>
{{ activeScheduleGameMap.get(game.id)!.status === 'active' ? 'In Progress' : 'Resume' }}
</NuxtLink>
</template>
<!-- Incomplete game: show Play button --> <!-- Incomplete game: show Play button -->
<template v-else> <template v-else>
<span class="flex-1"></span> <span class="flex-1"></span>
<button <button
@click="handlePlayScheduledGame(game.home_team.id, game.away_team.id)" @click="handlePlayScheduledGame(game.home_team.id, game.away_team.id, game.id)"
:disabled="isCreatingQuickGame" :disabled="isCreatingQuickGame"
class="px-2 py-1 text-xs bg-primary hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium rounded transition disabled:cursor-not-allowed" class="px-2 py-1 text-xs bg-primary hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium rounded transition disabled:cursor-not-allowed"
> >
@ -382,6 +392,23 @@ const groupedScheduleGames = computed<ScheduleGroup[]>(() => {
return Array.from(groups.values()).sort((a, b) => a.games[0].id - b.games[0].id) return Array.from(groups.values()).sort((a, b) => a.games[0].id - b.games[0].id)
}) })
// Map of schedule_game_id -> webapp game for active games
// Used to show indicators on schedule entries that have webapp games in progress
const activeScheduleGameMap = computed(() => {
const map = new Map<number, { game_id: string; status: string }>()
if (!games.value) return map
for (const game of games.value) {
if (game.schedule_game_id && (game.status === 'active' || game.status === 'pending')) {
map.set(game.schedule_game_id, {
game_id: game.game_id,
status: game.status,
})
}
}
return map
})
// Quick-create a demo game with pre-configured lineups // Quick-create a demo game with pre-configured lineups
async function handleQuickCreate() { async function handleQuickCreate() {
try { try {
@ -411,12 +438,12 @@ async function handleQuickCreate() {
} }
// Create a game from a scheduled matchup // Create a game from a scheduled matchup
async function handlePlayScheduledGame(homeTeamId: number, awayTeamId: number) { async function handlePlayScheduledGame(homeTeamId: number, awayTeamId: number, scheduleGameId: number) {
try { try {
isCreatingQuickGame.value = true isCreatingQuickGame.value = true
error.value = null error.value = null
console.log(`[Games Page] Creating game: ${awayTeamId} @ ${homeTeamId}`) console.log(`[Games Page] Creating game: ${awayTeamId} @ ${homeTeamId} (schedule_game_id: ${scheduleGameId})`)
const response = await $fetch<{ game_id: string; message: string; status: string }>( const response = await $fetch<{ game_id: string; message: string; status: string }>(
`${config.public.apiUrl}/api/games/quick-create`, `${config.public.apiUrl}/api/games/quick-create`,
@ -426,6 +453,7 @@ async function handlePlayScheduledGame(homeTeamId: number, awayTeamId: number) {
body: { body: {
home_team_id: homeTeamId, home_team_id: homeTeamId,
away_team_id: awayTeamId, away_team_id: awayTeamId,
schedule_game_id: scheduleGameId,
}, },
} }
) )

View File

@ -301,6 +301,7 @@ export interface ManualOutcomeSubmission {
/** /**
* Game list item for active/completed games * Game list item for active/completed games
* Note: Field names match backend GameListItem response
*/ */
export interface GameListItem { export interface GameListItem {
game_id: string game_id: string
@ -308,12 +309,17 @@ export interface GameListItem {
home_team_id: number home_team_id: number
away_team_id: number away_team_id: number
status: GameStatus status: GameStatus
current_inning: number // Enriched team info from backend
current_half: InningHalf home_team_name?: string
away_team_name?: string
home_team_abbrev?: string
away_team_abbrev?: string
home_score: number home_score: number
away_score: number away_score: number
started_at: string | null inning?: number
completed_at: string | null half?: InningHalf
// External schedule reference (for linking to SBA/PD schedule systems)
schedule_game_id?: number
} }
/** /**