CLAUDE: Add Undo Last Play feature for game rollback

- Added rollback_play WebSocket handler (handlers.py:1632)
  - Accepts game_id and num_plays (default: 1)
  - Validates game state and play count
  - Broadcasts play_rolled_back and game_state_update events
  - Full error handling with rate limiting

- Added undoLastPlay action to useGameActions composable
  - Emits rollback_play event to backend

- Added Undo button to game page ([id].vue)
  - Amber floating action button with undo arrow icon
  - Positioned above substitutions button
  - Only visible when game is active and has plays

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2025-11-27 21:34:48 -06:00
parent c27a652e54
commit 920d1c599c
3 changed files with 720 additions and 229 deletions

File diff suppressed because it is too large Load Diff

View File

@ -246,6 +246,27 @@ export function useGameActions(gameId?: string) {
uiStore.showInfo('Requesting pitching change...', 3000)
}
// ============================================================================
// Undo/Rollback Actions
// ============================================================================
/**
* Undo the last N plays
* Rolls back plays from the database and reconstructs game state
*/
function undoLastPlay(numPlays: number = 1) {
if (!validateConnection()) return
console.log('[GameActions] Undoing last', numPlays, 'play(s)')
socket.value!.emit('rollback_play', {
game_id: currentGameId.value!,
num_plays: numPlays,
})
uiStore.showInfo(`Undoing ${numPlays} play(s)...`, 3000)
}
// ============================================================================
// Data Request Actions
// ============================================================================
@ -314,6 +335,9 @@ export function useGameActions(gameId?: string) {
requestDefensiveReplacement,
requestPitchingChange,
// Undo/Rollback
undoLastPlay,
// Data requests
getLineup,
getBoxScore,

View File

@ -238,17 +238,33 @@
</div>
</Teleport>
<!-- Floating Action Button for Substitutions -->
<button
v-if="canMakeSubstitutions"
class="fixed bottom-6 right-6 w-16 h-16 bg-primary hover:bg-blue-700 text-white rounded-full shadow-lg flex items-center justify-center z-40 transition-all hover:scale-110"
aria-label="Open Substitutions"
@click="showSubstitutions = true"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
</button>
<!-- Floating Action Buttons -->
<div class="fixed bottom-6 right-6 flex flex-col gap-3 z-40">
<!-- Undo Last Play Button -->
<button
v-if="canUndo"
class="w-14 h-14 bg-amber-500 hover:bg-amber-600 text-white rounded-full shadow-lg flex items-center justify-center transition-all hover:scale-110"
aria-label="Undo Last Play"
title="Undo Last Play"
@click="handleUndoLastPlay"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
</svg>
</button>
<!-- Substitutions Button -->
<button
v-if="canMakeSubstitutions"
class="w-16 h-16 bg-primary hover:bg-blue-700 text-white rounded-full shadow-lg flex items-center justify-center transition-all hover:scale-110"
aria-label="Open Substitutions"
@click="showSubstitutions = true"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
</button>
</div>
</div>
</template>
@ -290,6 +306,9 @@ const { socket, isConnected, connectionError, connect } = useWebSocket()
// useGameActions will create its own computed internally if needed
const actions = useGameActions(route.params.id as string)
// Destructure undoLastPlay for the undo button
const { undoLastPlay } = actions
// Game state from store
const gameState = computed(() => gameStore.gameState)
const playHistory = computed(() => gameStore.playHistory)
@ -395,6 +414,11 @@ const canMakeSubstitutions = computed(() => {
return gameState.value?.status === 'active' && isMyTurn.value
})
const canUndo = computed(() => {
// Can only undo if game is active and there are plays to undo
return gameState.value?.status === 'active' && (gameState.value?.play_count ?? 0) > 0
})
// Lineup helpers for substitutions
const currentLineup = computed(() => {
if (!myTeamId.value) return []
@ -543,6 +567,12 @@ const handleSubstitutionCancel = () => {
showSubstitutions.value = false
}
// Undo handler
const handleUndoLastPlay = () => {
console.log('[Game Page] Undoing last play')
undoLastPlay(1)
}
// Measure ScoreBoard height dynamically
const updateScoreBoardHeight = () => {
if (scoreBoardRef.value) {