Improve API error handling for validation errors

- Parse FastAPI 422 validation error arrays into readable messages
- Update ErrorResponse type to handle both string and array detail
- Use inline type for validation error objects (no any)
- Display field-level validation errors instead of [object Object]

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-02-01 20:50:03 -06:00
parent 52c8edcf93
commit fdb5225356
4 changed files with 53 additions and 7 deletions

View File

@ -48,7 +48,7 @@ async function parseErrorResponse(response: Response): Promise<ApiError> {
if (Array.isArray(data.detail)) {
// Extract error messages from validation error array
detail = data.detail
.map((err: any) => {
.map((err: { loc?: string[]; msg?: string; type?: string }) => {
const field = err.loc ? err.loc.join('.') : 'unknown'
return `${field}: ${err.msg || 'Invalid value'}`
})

View File

@ -9,6 +9,7 @@
import { ref, onMounted, onUnmounted, shallowRef } from 'vue'
import { createGame, scenes } from '@/game'
import { gameBridge } from '@/game/bridge'
import type Phaser from 'phaser'
/**
@ -68,10 +69,9 @@ function setupResizeObserver(): void {
resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
// Phaser handles resize internally with Scale.RESIZE mode,
// but we can emit events if needed for external coordination
// Emit resize events via gameBridge so MatchScene can respond
if (game.value && entry.contentRect) {
game.value.events.emit('resize', {
gameBridge.emit('resize', {
width: entry.contentRect.width,
height: entry.contentRect.height,
})

View File

@ -46,7 +46,7 @@
* ```
*/
import mitt, { type Emitter, type EventType, type Handler } from 'mitt'
import mitt, { type Emitter, type Handler } from 'mitt'
import type {
GameBridgeEvents,
@ -80,6 +80,7 @@ type BridgeEventMap = {
'card:clicked': CardClickEvent
'zone:clicked': ZoneClickEvent
'animation:complete': AnimationCompleteEvent
'attack:request': undefined
'ready': undefined
'error': Error
}

View File

@ -21,6 +21,7 @@ import type { VisibleGameState } from '@/types/game'
import { StateRenderer } from '../sync/StateRenderer'
import { Board, createBoard } from '../objects/Board'
import { calculateLayout } from '../layout'
import { HandManager } from '../interactions/HandManager'
// =============================================================================
// Constants
@ -90,6 +91,12 @@ export class MatchScene extends Phaser.Scene {
resize?: (data: ResizeEvent) => void
} = {}
/**
* Hand interaction manager.
* Null until StateRenderer creates the hand zone.
*/
private handManager: HandManager | null = null
// ===========================================================================
// Lifecycle Methods
// ===========================================================================
@ -296,9 +303,15 @@ export class MatchScene extends Phaser.Scene {
this.drawBoardBackground(width, height)
// Update board layout
const layout = calculateLayout(width, height)
if (this.board) {
const layout = calculateLayout(width, height)
this.board.updateLayout(layout)
this.board.setLayout(layout)
}
// Update HandManager layout
if (this.handManager) {
this.handManager.setLayout(layout)
}
// Re-render state if we have one
@ -328,6 +341,32 @@ export class MatchScene extends Phaser.Scene {
// Delegate to StateRenderer
if (this.stateRenderer) {
this.stateRenderer.render(state)
// Initialize HandManager if not already created
if (!this.handManager) {
const myZones = this.stateRenderer.getPlayerZones(true)
if (myZones) {
this.handManager = new HandManager(this, myZones.hand)
// Set initial layout
const { width, height } = this.cameras.main
const layout = calculateLayout(width, height)
this.handManager.setLayout(layout)
}
}
// Update HandManager with current state
if (this.handManager) {
// Update card registry for validation
this.handManager.setCardRegistry(state.card_registry)
// Enable/disable interactions based on turn state
if (state.is_my_turn && !state.winner_id) {
this.handManager.enableHandInteractions()
} else {
this.handManager.disableHandInteractions()
}
}
}
// Log state update for debugging
@ -400,6 +439,12 @@ export class MatchScene extends Phaser.Scene {
* Properly destroys all Phaser objects to prevent memory leaks.
*/
clearBoard(): void {
// Clear HandManager
if (this.handManager) {
this.handManager.destroy()
this.handManager = null
}
// Clear StateRenderer
if (this.stateRenderer) {
this.stateRenderer.clear()