CLAUDE: Store team display info in DB and fix lineup auto-start

Backend:
- Add game_metadata to create_game() and quick_create_game() endpoints
- Fetch team display info (lname, sname, abbrev, color, thumbnail) from
  SBA API at game creation time and store in DB
- Populate GameState with team display fields from game_metadata
- Fix submit_team_lineup to cache lineup in state_manager after DB write
  so auto-start correctly detects both teams ready

Frontend:
- Read team colors/names/thumbnails from gameState instead of useState
- Remove useState approach that failed across SSR navigation
- Fix create.vue redirect from legacy /games/lineup/[id] to /games/[id]
- Update game.vue header to show team names from gameState

Docs:
- Update CLAUDE.md to note dev mode has broken auth, always use prod

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-01-17 08:43:26 -06:00
parent ff3f1746d6
commit d60b7a2d60
15 changed files with 247 additions and 103 deletions

View File

@ -92,13 +92,15 @@ strat-gameplay-webapp/
The entire stack runs in Docker with a single command. No local Python or Node.js required.
```bash
# Development (hot-reload enabled)
./start.sh dev
> **⚠️ ALWAYS USE PROD MODE**: Discord OAuth does not work in dev mode due to cookie/CORS configuration. Since this system isn't live yet, always build and run with `prod` mode for testing.
# Production (optimized build)
```bash
# Production (optimized build) - USE THIS
./start.sh prod
# Development - DO NOT USE (auth broken)
# ./start.sh dev
# Stop all services
./start.sh stop
@ -116,8 +118,8 @@ The entire stack runs in Docker with a single command. No local Python or Node.j
| Mode | Backend | Frontend | Use Case |
|------|---------|----------|----------|
| `dev` | Hot-reload (uvicorn --reload) | Hot-reload (nuxt dev) | Active development |
| `prod` | Production build | SSR optimized build | Demo/deployment |
| `prod` | Production build | SSR optimized build | **Always use this** - auth works correctly |
| `dev` | Hot-reload (uvicorn --reload) | Hot-reload (nuxt dev) | ❌ Auth broken - do not use |
### Service URLs

View File

@ -320,12 +320,38 @@ async def create_game(request: CreateGameRequest):
status_code=400, detail="Home and away teams must be different"
)
# Fetch team display info from SBA API and build game_metadata
# This persists team data so it's always available regardless of season
teams_data = await sba_api_client.get_teams_by_ids(
[request.home_team_id, request.away_team_id], season=request.season
)
home_team_data = teams_data.get(request.home_team_id, {})
away_team_data = teams_data.get(request.away_team_id, {})
game_metadata = {
"home_team": {
"lname": home_team_data.get("lname"),
"sname": home_team_data.get("sname"),
"abbrev": home_team_data.get("abbrev"),
"color": home_team_data.get("color"),
"thumbnail": home_team_data.get("thumbnail"),
},
"away_team": {
"lname": away_team_data.get("lname"),
"sname": away_team_data.get("sname"),
"abbrev": away_team_data.get("abbrev"),
"color": away_team_data.get("color"),
"thumbnail": away_team_data.get("thumbnail"),
},
}
# Create game in state manager (in-memory)
state = await state_manager.create_game(
game_id=game_id,
league_id=request.league_id,
home_team_id=request.home_team_id,
away_team_id=request.away_team_id,
game_metadata=game_metadata,
)
# Save to database
@ -337,6 +363,7 @@ async def create_game(request: CreateGameRequest):
away_team_id=request.away_team_id,
game_mode="friendly" if not request.is_ai_opponent else "ai",
visibility="public",
game_metadata=game_metadata,
)
logger.info(
@ -407,6 +434,31 @@ async def quick_create_game(
f"(creator: {creator_discord_id}, custom_teams: {use_custom_teams})"
)
# Fetch team display info from SBA API and build game_metadata
# This persists team data so it's always available regardless of season
teams_data = await sba_api_client.get_teams_by_ids(
[home_team_id, away_team_id], season=13
)
home_team_data = teams_data.get(home_team_id, {})
away_team_data = teams_data.get(away_team_id, {})
game_metadata = {
"home_team": {
"lname": home_team_data.get("lname"),
"sname": home_team_data.get("sname"),
"abbrev": home_team_data.get("abbrev"),
"color": home_team_data.get("color"),
"thumbnail": home_team_data.get("thumbnail"),
},
"away_team": {
"lname": away_team_data.get("lname"),
"sname": away_team_data.get("sname"),
"abbrev": away_team_data.get("abbrev"),
"color": away_team_data.get("color"),
"thumbnail": away_team_data.get("thumbnail"),
},
}
# Create game in state manager
state = await state_manager.create_game(
game_id=game_id,
@ -414,6 +466,7 @@ async def quick_create_game(
home_team_id=home_team_id,
away_team_id=away_team_id,
creator_discord_id=creator_discord_id,
game_metadata=game_metadata,
)
# Save to database
@ -426,6 +479,7 @@ async def quick_create_game(
game_mode="friendly",
visibility="public",
schedule_game_id=schedule_game_id,
game_metadata=game_metadata,
)
if use_custom_teams:
@ -1023,6 +1077,16 @@ async def submit_team_lineup(game_id: str, request: SubmitTeamLineupRequest):
logger.info(f"Added {player_count} players to team {request.team_id} lineup")
# Load lineup from DB and cache in state_manager for subsequent checks
team_lineup = await lineup_service.load_team_lineup_with_player_data(
game_id=game_uuid,
team_id=request.team_id,
league_id=state.league_id,
)
if team_lineup:
state_manager.set_lineup(game_uuid, request.team_id, team_lineup)
logger.info(f"Cached lineup for team {request.team_id} in state_manager")
# Check if both teams now have lineups
home_lineup = state_manager.get_lineup(game_uuid, state.home_team_id)
away_lineup = state_manager.get_lineup(game_uuid, state.away_team_id)

View File

@ -18,6 +18,7 @@ class TeamResponse(BaseModel):
sname: str
lname: str
color: str | None = None
thumbnail: str | None = None
manager_legacy: str | None = None
gmid: str | None = None
gmid2: str | None = None
@ -46,6 +47,7 @@ async def get_teams(season: int = Query(..., description="Season number (e.g., 3
sname=team["sname"],
lname=team["lname"],
color=team.get("color"),
thumbnail=team.get("thumbnail"),
manager_legacy=team.get("manager_legacy"),
gmid=team.get("gmid"),
gmid2=team.get("gmid2"),

View File

@ -94,6 +94,7 @@ class StateManager:
away_team_is_ai: bool = False,
auto_mode: bool = False,
creator_discord_id: str | None = None,
game_metadata: dict | None = None,
) -> GameState:
"""
Create a new game state in memory.
@ -106,6 +107,7 @@ class StateManager:
home_team_is_ai: Whether home team is AI-controlled
away_team_is_ai: Whether away team is AI-controlled
auto_mode: True = auto-generate outcomes (PD only), False = manual submissions
game_metadata: Optional dict with team display info (lname, abbrev, color, thumbnail)
Returns:
Newly created GameState
@ -127,6 +129,10 @@ class StateManager:
lineup_id=0, card_id=0, position="DH", batting_order=None
)
# Extract team display info from metadata
home_meta = game_metadata.get("home_team", {}) if game_metadata else {}
away_meta = game_metadata.get("away_team", {}) if game_metadata else {}
state = GameState(
game_id=game_id,
league_id=league_id,
@ -137,6 +143,15 @@ class StateManager:
auto_mode=auto_mode,
creator_discord_id=creator_discord_id,
current_batter=placeholder_batter, # Will be replaced by _prepare_next_play() when game starts
# Team display info from metadata
home_team_name=home_meta.get("lname"),
home_team_abbrev=home_meta.get("abbrev"),
home_team_color=home_meta.get("color"),
home_team_thumbnail=home_meta.get("thumbnail"),
away_team_name=away_meta.get("lname"),
away_team_abbrev=away_meta.get("abbrev"),
away_team_color=away_meta.get("color"),
away_team_thumbnail=away_meta.get("thumbnail"),
)
self._states[game_id] = state

View File

@ -103,6 +103,7 @@ class DatabaseOperations:
away_team_is_ai: bool = False,
ai_difficulty: str | None = None,
schedule_game_id: int | None = None,
game_metadata: dict | None = None,
) -> Game:
"""
Create new game in database.
@ -118,6 +119,7 @@ class DatabaseOperations:
away_team_is_ai: Whether away team is AI
ai_difficulty: AI difficulty if applicable
schedule_game_id: External schedule game ID for linking (SBA, PD, etc.)
game_metadata: Optional dict with team display info (lname, abbrev, color, thumbnail)
Returns:
Created Game model
@ -137,6 +139,7 @@ class DatabaseOperations:
away_team_is_ai=away_team_is_ai,
ai_difficulty=ai_difficulty,
schedule_game_id=schedule_game_id,
game_metadata=game_metadata or {},
status="pending",
)
session.add(game)

View File

@ -396,6 +396,16 @@ class GameState(BaseModel):
home_team_is_ai: bool = False
away_team_is_ai: bool = False
# Team display info (for UI - fetched from league API when game created)
home_team_name: str | None = None # e.g., "Chicago Cyclones"
home_team_abbrev: str | None = None # e.g., "CHC"
home_team_color: str | None = None # e.g., "ff5349" (no # prefix)
home_team_thumbnail: str | None = None # Team logo URL
away_team_name: str | None = None
away_team_abbrev: str | None = None
away_team_color: str | None = None
away_team_thumbnail: str | None = None
# Creator (for demo/testing - creator can control home team)
creator_discord_id: str | None = None

View File

@ -55,9 +55,13 @@ frontend-sba/
## Development
> **⚠️ ALWAYS USE PROD MODE**: Run the full stack via Docker with `./start.sh prod` from the project root. Dev mode (`./start.sh dev`) has broken Discord OAuth due to cookie/CORS issues. The system isn't live yet, so always use prod for testing.
```bash
npm install # First time
npm run dev # Dev server at http://localhost:3000
# From project root - use this for testing
./start.sh prod
# Local commands (for type checking only, not running)
npm run type-check # Check types
npm run lint # Lint code
```

View File

@ -11,13 +11,14 @@
<!-- Pitcher Image/Badge -->
<div class="w-12 h-12 rounded-full flex items-center justify-center shadow-lg flex-shrink-0 overflow-hidden">
<img
v-if="pitcherPlayer?.headshot"
:src="pitcherPlayer.headshot"
v-if="getPlayerPreviewImage(pitcherPlayer)"
:src="getPlayerPreviewImage(pitcherPlayer)!"
:alt="pitcherName"
class="w-full h-full object-cover"
@error="(e) => (e.target as HTMLImageElement).style.display = 'none'"
>
<div v-else class="w-full h-full bg-blue-500 flex items-center justify-center text-white font-bold text-lg">
P
<div v-else class="w-full h-full bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center text-white font-bold text-lg">
{{ getPlayerFallbackInitial(pitcherPlayer) }}
</div>
</div>
@ -52,13 +53,14 @@
<!-- Batter Image/Badge -->
<div class="w-12 h-12 rounded-full flex items-center justify-center shadow-lg flex-shrink-0 overflow-hidden">
<img
v-if="batterPlayer?.headshot"
:src="batterPlayer.headshot"
v-if="getPlayerPreviewImage(batterPlayer)"
:src="getPlayerPreviewImage(batterPlayer)!"
:alt="batterName"
class="w-full h-full object-cover"
@error="(e) => (e.target as HTMLImageElement).style.display = 'none'"
>
<div v-else class="w-full h-full bg-red-500 flex items-center justify-center text-white font-bold text-lg">
B
<div v-else class="w-full h-full bg-gradient-to-br from-red-500 to-red-600 flex items-center justify-center text-white font-bold text-lg">
{{ getPlayerFallbackInitial(batterPlayer) }}
</div>
</div>
@ -90,13 +92,14 @@
<!-- Pitcher Image/Badge -->
<div class="w-16 h-16 rounded-full flex items-center justify-center shadow-xl flex-shrink-0 overflow-hidden">
<img
v-if="pitcherPlayer?.headshot"
:src="pitcherPlayer.headshot"
v-if="getPlayerPreviewImage(pitcherPlayer)"
:src="getPlayerPreviewImage(pitcherPlayer)!"
:alt="pitcherName"
class="w-full h-full object-cover"
@error="(e) => (e.target as HTMLImageElement).style.display = 'none'"
>
<div v-else class="w-full h-full bg-blue-500 flex items-center justify-center text-white font-bold text-2xl">
P
<div v-else class="w-full h-full bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center text-white font-bold text-2xl">
{{ getPlayerFallbackInitial(pitcherPlayer) }}
</div>
</div>
@ -124,13 +127,14 @@
<!-- Batter Image/Badge -->
<div class="w-16 h-16 rounded-full flex items-center justify-center shadow-xl flex-shrink-0 overflow-hidden">
<img
v-if="batterPlayer?.headshot"
:src="batterPlayer.headshot"
v-if="getPlayerPreviewImage(batterPlayer)"
:src="getPlayerPreviewImage(batterPlayer)!"
:alt="batterName"
class="w-full h-full object-cover"
@error="(e) => (e.target as HTMLImageElement).style.display = 'none'"
>
<div v-else class="w-full h-full bg-red-500 flex items-center justify-center text-white font-bold text-2xl">
B
<div v-else class="w-full h-full bg-gradient-to-br from-red-500 to-red-600 flex items-center justify-center text-white font-bold text-2xl">
{{ getPlayerFallbackInitial(batterPlayer) }}
</div>
</div>
@ -221,6 +225,25 @@ const pitcherName = computed(() => {
if (!props.currentPitcher) return 'Unknown Pitcher'
return `Player #${props.currentPitcher.card_id || props.currentPitcher.lineup_id}`
})
// Get player preview image with fallback priority: headshot > vanity_card > null
function getPlayerPreviewImage(player: { headshot?: string | null; vanity_card?: string | null } | null): string | null {
if (!player) return null
return player.headshot || player.vanity_card || null
}
// Get player avatar fallback - use first + last initials (e.g., "Alex Verdugo" -> "AV")
// Ignores common suffixes like Jr, Sr, II, III, IV
function getPlayerFallbackInitial(player: { name: string } | null): string {
if (!player) return '?'
const suffixes = ['jr', 'jr.', 'sr', 'sr.', 'ii', 'iii', 'iv', 'v']
const parts = player.name.trim().split(/\s+/).filter(
part => !suffixes.includes(part.toLowerCase())
)
if (parts.length === 0) return '?'
if (parts.length === 1) return parts[0].charAt(0).toUpperCase()
return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase()
}
</script>
<style scoped>

View File

@ -14,9 +14,17 @@
<!-- Score Display with Game Situation -->
<div class="flex items-center justify-between">
<!-- Away Team -->
<div class="flex-1 text-center">
<div class="text-xs font-medium text-blue-100 mb-1">AWAY</div>
<div class="text-4xl font-bold tabular-nums">{{ awayScore }}</div>
<div class="flex-1 text-center relative">
<img
v-if="awayTeamThumbnail"
:src="awayTeamThumbnail"
alt=""
class="absolute inset-0 w-full h-full object-contain opacity-15 pointer-events-none"
>
<div class="relative">
<div class="text-xs font-medium text-blue-100 mb-1">AWAY</div>
<div class="text-4xl font-bold tabular-nums">{{ awayScore }}</div>
</div>
</div>
<!-- Center: Inning + Runners/Outs -->
@ -77,9 +85,17 @@
</div>
<!-- Home Team -->
<div class="flex-1 text-center">
<div class="text-xs font-medium text-blue-100 mb-1">HOME</div>
<div class="text-4xl font-bold tabular-nums">{{ homeScore }}</div>
<div class="flex-1 text-center relative">
<img
v-if="homeTeamThumbnail"
:src="homeTeamThumbnail"
alt=""
class="absolute inset-0 w-full h-full object-contain opacity-15 pointer-events-none"
>
<div class="relative">
<div class="text-xs font-medium text-blue-100 mb-1">HOME</div>
<div class="text-4xl font-bold tabular-nums">{{ homeScore }}</div>
</div>
</div>
</div>
</div>
@ -88,9 +104,17 @@
<div class="hidden lg:flex items-center justify-between">
<!-- Left: Away Team Score -->
<div class="flex items-center gap-4">
<div class="text-center min-w-[100px]">
<div class="text-sm font-medium text-blue-100">AWAY</div>
<div class="text-5xl font-bold tabular-nums">{{ awayScore }}</div>
<div class="text-center min-w-[100px] relative">
<img
v-if="awayTeamThumbnail"
:src="awayTeamThumbnail"
alt=""
class="absolute inset-0 w-full h-full object-contain opacity-15 pointer-events-none"
>
<div class="relative">
<div class="text-sm font-medium text-blue-100">AWAY</div>
<div class="text-5xl font-bold tabular-nums">{{ awayScore }}</div>
</div>
</div>
</div>
@ -153,9 +177,17 @@
<!-- Right: Home Team Score -->
<div class="flex items-center gap-4">
<div class="text-center min-w-[100px]">
<div class="text-sm font-medium text-blue-100">HOME</div>
<div class="text-5xl font-bold tabular-nums">{{ homeScore }}</div>
<div class="text-center min-w-[100px] relative">
<img
v-if="homeTeamThumbnail"
:src="homeTeamThumbnail"
alt=""
class="absolute inset-0 w-full h-full object-contain opacity-15 pointer-events-none"
>
<div class="relative">
<div class="text-sm font-medium text-blue-100">HOME</div>
<div class="text-5xl font-bold tabular-nums">{{ homeScore }}</div>
</div>
</div>
</div>
</div>
@ -180,6 +212,8 @@ interface Props {
}
awayTeamColor?: string
homeTeamColor?: string
awayTeamThumbnail?: string | null
homeTeamThumbnail?: string | null
}
const props = withDefaults(defineProps<Props>(), {
@ -190,7 +224,9 @@ const props = withDefaults(defineProps<Props>(), {
outs: 0,
runners: () => ({ first: false, second: false, third: false }),
awayTeamColor: undefined,
homeTeamColor: undefined
homeTeamColor: undefined,
awayTeamThumbnail: undefined,
homeTeamThumbnail: undefined
})
// Generate gradient style from team colors

View File

@ -24,8 +24,10 @@
<span>Back to Games</span>
</NuxtLink>
<!-- Logo -->
<div class="text-lg font-bold">SBA</div>
<!-- Matchup -->
<div class="text-sm font-bold text-center truncate max-w-[200px] sm:max-w-none sm:text-lg">
{{ matchupText }}
</div>
<!-- Connection Status -->
<div class="flex items-center space-x-3">
@ -67,12 +69,23 @@
<script setup lang="ts">
import { useAuthStore } from '~/store/auth'
import { useGameStore } from '~/store/game'
import { useWebSocket } from '~/composables/useWebSocket'
const authStore = useAuthStore()
const gameStore = useGameStore()
// WebSocket connection status - use composable directly as source of truth
const { isConnected } = useWebSocket()
// Team names for header (from gameState, stored in DB at game creation)
const matchupText = computed(() => {
const gs = gameStore.gameState
if (gs?.away_team_name && gs?.home_team_name) {
return `${gs.away_team_name} @ ${gs.home_team_name}`
}
return 'SBA'
})
</script>
<style scoped>

View File

@ -10,6 +10,8 @@
:runners="runnersState"
:away-team-color="awayTeamColor"
:home-team-color="homeTeamColor"
:away-team-thumbnail="awayTeamThumbnail"
:home-team-thumbnail="homeTeamThumbnail"
/>
<!-- Tab Navigation (sticky below header) -->
@ -67,7 +69,6 @@
import { useGameStore } from '~/store/game'
import { useAuthStore } from '~/store/auth'
import { useUiStore } from '~/store/ui'
import type { SbaTeam } from '~/types/api'
import ScoreBoard from '~/components/Game/ScoreBoard.vue'
import GamePlay from '~/components/Game/GamePlay.vue'
import LineupBuilder from '~/components/Game/LineupBuilder.vue'
@ -78,17 +79,13 @@ definePageMeta({
middleware: ['auth'],
})
// Config
const config = useRuntimeConfig()
// Stores
const route = useRoute()
const gameStore = useGameStore()
const authStore = useAuthStore()
const uiStore = useUiStore()
// Team data for colors
const teamsMap = ref<Map<number, SbaTeam>>(new Map())
// Note: Team display info now comes directly from gameState (stored in DB at game creation)
// Game ID from route
const gameId = computed(() => route.params.id as string)
@ -121,62 +118,23 @@ const runnersState = computed(() => {
}
})
// Team colors for ScoreBoard gradient
// Team colors for ScoreBoard gradient (from gameState, stored in DB at creation)
const awayTeamColor = computed(() => {
if (!gameState.value) return undefined
return teamsMap.value.get(gameState.value.away_team_id)?.color
const color = gameState.value?.away_team_color
// Add # prefix if color exists but doesn't have it
return color ? (color.startsWith('#') ? color : `#${color}`) : undefined
})
const homeTeamColor = computed(() => {
if (!gameState.value) return undefined
return teamsMap.value.get(gameState.value.home_team_id)?.color
const color = gameState.value?.home_team_color
// Add # prefix if color exists but doesn't have it
return color ? (color.startsWith('#') ? color : `#${color}`) : undefined
})
// Get season from schedule state (already fetched on /games page)
const selectedSeason = useState<number>('schedule-season')
// Team thumbnails for ScoreBoard
const awayTeamThumbnail = computed(() => gameState.value?.away_team_thumbnail)
// Fetch team data when game state becomes available
watch(gameState, async (state) => {
if (!state) return
const teamIds = [state.home_team_id, state.away_team_id]
// Only fetch teams we don't have yet
const missingIds = teamIds.filter(id => !teamsMap.value.has(id))
if (missingIds.length === 0) return
try {
// Get season - use cached value or fetch current
let season = selectedSeason.value
if (!season) {
console.log('[Game Page] No cached season, fetching current...')
const currentInfo = await $fetch<{ season: number; week: number }>(`${config.public.apiUrl}/api/schedule/current`, {
credentials: 'include'
})
season = currentInfo.season
selectedSeason.value = season // Cache it
}
console.log('[Game Page] Fetching teams for season', season)
// Fetch all teams for the season and filter to the ones we need
const teams = await $fetch<SbaTeam[]>(`${config.public.apiUrl}/api/teams/`, {
credentials: 'include',
query: { season }
})
console.log('[Game Page] Got teams, looking for IDs:', teamIds)
for (const team of teams) {
if (teamIds.includes(team.id)) {
console.log('[Game Page] Found team:', team.id, team.sname, 'color:', team.color)
teamsMap.value.set(team.id, team)
}
}
} catch (error) {
console.warn('[Game Page] Failed to fetch team data for colors:', error)
}
}, { immediate: true })
const homeTeamThumbnail = computed(() => gameState.value?.home_team_thumbnail)
// Check if user is a manager of either team in this game
const isUserManager = computed(() => {

View File

@ -174,8 +174,8 @@ const handleCreateGame = async () => {
}
)
// Redirect to lineup builder
router.push(`/games/lineup/${response.game_id}`)
// Redirect to game page (lineup is a tab there)
router.push(`/games/${response.game_id}`)
} catch (err: any) {
error.value = err.data?.detail || err.message || 'Failed to create game'
console.error('Create game error:', err)

View File

@ -144,7 +144,7 @@
Final
</span>
</template>
<!-- Active webapp game: show "In Progress" link -->
<!-- Active webapp game: show "In Progress" link (team data stored in DB) -->
<template v-else-if="activeScheduleGameMap.get(game.id)">
<span class="flex-1"></span>
<NuxtLink
@ -158,7 +158,7 @@
<template v-else>
<span class="flex-1"></span>
<button
@click="handlePlayScheduledGame(game.home_team.id, game.away_team.id, game.id)"
@click="handlePlayScheduledGame(game)"
: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"
>
@ -334,6 +334,7 @@
<script setup lang="ts">
import { useAuthStore } from '~/store/auth'
import type { SbaScheduledGame } from '~/types/schedule'
definePageMeta({
middleware: ['auth'], // Require authentication
@ -438,12 +439,14 @@ async function handleQuickCreate() {
}
// Create a game from a scheduled matchup
async function handlePlayScheduledGame(homeTeamId: number, awayTeamId: number, scheduleGameId: number) {
// Note: Team display info is now stored in DB by backend - no need to pass via useState
async function handlePlayScheduledGame(scheduledGame: SbaScheduledGame) {
try {
isCreatingQuickGame.value = true
error.value = null
console.log(`[Games Page] Creating game: ${awayTeamId} @ ${homeTeamId} (schedule_game_id: ${scheduleGameId})`)
const { home_team, away_team, id: scheduleGameId } = scheduledGame
console.log(`[Games Page] Creating game: ${away_team.sname} @ ${home_team.sname} (schedule_game_id: ${scheduleGameId})`)
const response = await $fetch<{ game_id: string; message: string; status: string }>(
`${config.public.apiUrl}/api/games/quick-create`,
@ -451,8 +454,8 @@ async function handlePlayScheduledGame(homeTeamId: number, awayTeamId: number, s
method: 'POST',
credentials: 'include',
body: {
home_team_id: homeTeamId,
away_team_id: awayTeamId,
home_team_id: home_team.id,
away_team_id: away_team.id,
schedule_game_id: scheduleGameId,
},
}
@ -460,7 +463,7 @@ async function handlePlayScheduledGame(homeTeamId: number, awayTeamId: number, s
console.log('[Games Page] Created game from schedule:', response)
// Redirect to game page
// Redirect to game page (team display info is stored in DB by backend)
router.push(`/games/${response.game_id}`)
} catch (err: any) {
console.error('[Games Page] Failed to create game from schedule:', err)

View File

@ -95,6 +95,7 @@ export interface SbaTeam {
sname: string // Short name (e.g., "Geese")
lname: string // Long name (e.g., "Everett Geese")
color: string // Hex color code
thumbnail: string | null // Team logo URL
manager_legacy: string
gmid: string | null
gmid2: string | null

View File

@ -72,6 +72,16 @@ export interface GameState {
home_team_is_ai: boolean
away_team_is_ai: boolean
// Team display info (from game_metadata, stored at creation time)
home_team_name?: string | null // Full name: "Chicago Cyclones"
home_team_abbrev?: string | null // Abbreviation: "CHC"
home_team_color?: string | null // Hex color without #: "ff5349"
home_team_thumbnail?: string | null // Team logo URL
away_team_name?: string | null
away_team_abbrev?: string | null
away_team_color?: string | null
away_team_thumbnail?: string | null
// Creator (for demo/testing - creator can control home team)
creator_discord_id: string | null