""" Development-only views for testing API functionality. This blueprint only loads when FLASK_ENV=development. Provides HTMX-based testing interfaces for API endpoints. """ from flask import Blueprint, render_template, request, jsonify import structlog from ..utils.api_client import get_api_client, APIError, APINotFoundError from ..utils.auth import require_auth_web as require_auth, get_current_user logger = structlog.get_logger(__name__) dev_bp = Blueprint('dev', __name__, url_prefix='/dev') @dev_bp.route('/') def index(): """Dev tools hub - links to all testing interfaces.""" return render_template('dev/index.html') @dev_bp.route('/story') @require_auth def story_hub(): """Story testing hub - select character and create/load sessions.""" client = get_api_client() try: # Get user's characters characters_response = client.get('/api/v1/characters') result = characters_response.get('result', {}) characters = result.get('characters', []) # Get user's active sessions (if endpoint exists) sessions = [] try: sessions_response = client.get('/api/v1/sessions') sessions = sessions_response.get('result', []) except (APINotFoundError, APIError): # Sessions list endpoint may not exist yet or has issues pass return render_template( 'dev/story.html', characters=characters, sessions=sessions ) except APIError as e: logger.error("failed_to_load_story_hub", error=str(e)) return render_template('dev/story.html', characters=[], sessions=[], error=str(e)) @dev_bp.route('/story/session/') @require_auth def story_session(session_id: str): """Story session gameplay interface.""" client = get_api_client() try: # Get session state session_response = client.get(f'/api/v1/sessions/{session_id}') session_data = session_response.get('result', {}) # Get session history history_response = client.get(f'/api/v1/sessions/{session_id}/history?limit=50') history_data = history_response.get('result', {}) # Get NPCs at current location npcs_present = [] game_state = session_data.get('game_state', {}) current_location = game_state.get('current_location_id') or game_state.get('current_location') if current_location: try: npcs_response = client.get(f'/api/v1/npcs/at-location/{current_location}') npcs_present = npcs_response.get('result', {}).get('npcs', []) except (APINotFoundError, APIError): # NPCs endpoint may not exist yet pass return render_template( 'dev/story_session.html', session=session_data, history=history_data.get('history', []), session_id=session_id, npcs_present=npcs_present ) except APINotFoundError: return render_template('dev/story.html', error=f"Session {session_id} not found"), 404 except APIError as e: logger.error("failed_to_load_session", session_id=session_id, error=str(e)) return render_template('dev/story.html', error=str(e)), 500 # HTMX Partial endpoints @dev_bp.route('/story/create-session', methods=['POST']) @require_auth def create_session(): """Create a new story session - returns HTMX partial.""" client = get_api_client() character_id = request.form.get('character_id') logger.info("create_session called", character_id=character_id, form_data=dict(request.form)) if not character_id: return '
No character selected
', 400 try: response = client.post('/api/v1/sessions', {'character_id': character_id}) session_data = response.get('result', {}) session_id = session_data.get('session_id') # Return redirect script to session page return f'''
Session created! Redirecting...
''' except APIError as e: logger.error("failed_to_create_session", character_id=character_id, error=str(e)) return f'
Failed to create session: {e}
', 500 @dev_bp.route('/story/action/', methods=['POST']) @require_auth def take_action(session_id: str): """Submit an action - returns job status partial for polling.""" client = get_api_client() action_type = request.form.get('action_type', 'button') prompt_id = request.form.get('prompt_id') custom_text = request.form.get('custom_text') question = request.form.get('question') payload = {'action_type': action_type} if action_type == 'button' and prompt_id: payload['prompt_id'] = prompt_id elif action_type == 'custom' and custom_text: payload['custom_text'] = custom_text elif action_type == 'ask_dm' and question: payload['question'] = question try: response = client.post(f'/api/v1/sessions/{session_id}/action', payload) result = response.get('result', {}) job_id = result.get('job_id') # Return polling partial return render_template( 'dev/partials/job_status.html', job_id=job_id, session_id=session_id, status='queued' ) except APIError as e: logger.error("failed_to_take_action", session_id=session_id, error=str(e)) return f'
Action failed: {e}
', 500 @dev_bp.route('/story/job-status/') @require_auth def job_status(job_id: str): """Poll job status - returns updated partial.""" client = get_api_client() session_id = request.args.get('session_id', '') try: response = client.get(f'/api/v1/jobs/{job_id}/status') result = response.get('result', {}) status = result.get('status', 'unknown') if status == 'completed': # Job done - return response # Check for NPC dialogue (in result.dialogue) vs story action (in dm_response) nested_result = result.get('result', {}) if nested_result.get('context_type') == 'npc_dialogue': # Use NPC dialogue template with conversation history return render_template( 'dev/partials/npc_dialogue.html', npc_name=nested_result.get('npc_name', 'NPC'), character_name=nested_result.get('character_name', 'You'), conversation_history=nested_result.get('conversation_history', []), player_line=nested_result.get('player_line', ''), dialogue=nested_result.get('dialogue', 'No response'), session_id=session_id ) else: dm_response = result.get('dm_response', 'No response') return render_template( 'dev/partials/dm_response.html', dm_response=dm_response, raw_result=result, session_id=session_id ) elif status in ('failed', 'error'): error_msg = result.get('error', 'Unknown error') return f'
Job failed: {error_msg}
' else: # Still processing - return polling partial return render_template( 'dev/partials/job_status.html', job_id=job_id, session_id=session_id, status=status ) except APIError as e: logger.error("failed_to_get_job_status", job_id=job_id, error=str(e)) return f'
Failed to get job status: {e}
', 500 @dev_bp.route('/story/history/') @require_auth def get_history(session_id: str): """Get session history - returns HTMX partial.""" client = get_api_client() limit = request.args.get('limit', 20, type=int) offset = request.args.get('offset', 0, type=int) try: response = client.get(f'/api/v1/sessions/{session_id}/history?limit={limit}&offset={offset}') result = response.get('result', {}) return render_template( 'dev/partials/history.html', history=result.get('history', []), pagination=result.get('pagination', {}), session_id=session_id ) except APIError as e: logger.error("failed_to_get_history", session_id=session_id, error=str(e)) return f'
Failed to load history: {e}
', 500 @dev_bp.route('/story/state/') @require_auth def get_state(session_id: str): """Get current session state - returns HTMX partial.""" client = get_api_client() try: response = client.get(f'/api/v1/sessions/{session_id}') session_data = response.get('result', {}) return render_template( 'dev/partials/session_state.html', session=session_data, session_id=session_id ) except APIError as e: logger.error("failed_to_get_state", session_id=session_id, error=str(e)) return f'
Failed to load state: {e}
', 500 # ===== NPC & Travel Endpoints ===== @dev_bp.route('/story/talk//', methods=['POST']) @require_auth def talk_to_npc(session_id: str, npc_id: str): """Talk to an NPC - returns dialogue response.""" client = get_api_client() # Support both topic (initial greeting) and player_response (conversation) player_response = request.form.get('player_response') topic = request.form.get('topic', 'greeting') try: payload = {'session_id': session_id} if player_response: # Player typed a custom response payload['player_response'] = player_response else: # Initial greeting click payload['topic'] = topic response = client.post(f'/api/v1/npcs/{npc_id}/talk', payload) result = response.get('result', {}) # Check if it's a job-based response (async) or immediate job_id = result.get('job_id') if job_id: return render_template( 'dev/partials/job_status.html', job_id=job_id, session_id=session_id, status='queued', is_npc_dialogue=True ) # Immediate response (if AI is sync or cached) dialogue = result.get('dialogue', result.get('response', 'The NPC has nothing to say.')) npc_name = result.get('npc_name', 'NPC') return f'''
{npc_name} says:
{dialogue}
''' except APINotFoundError: return '
NPC not found.
', 404 except APIError as e: logger.error("failed_to_talk_to_npc", session_id=session_id, npc_id=npc_id, error=str(e)) return f'
Failed to talk to NPC: {e}
', 500 @dev_bp.route('/story/travel-modal/') @require_auth def travel_modal(session_id: str): """Get travel modal with available locations.""" client = get_api_client() try: response = client.get(f'/api/v1/travel/available?session_id={session_id}') result = response.get('result', {}) available_locations = result.get('available_locations', []) return render_template( 'dev/partials/travel_modal.html', locations=available_locations, session_id=session_id ) except APIError as e: logger.error("failed_to_get_travel_options", session_id=session_id, error=str(e)) return f''' ''' @dev_bp.route('/story/travel/', methods=['POST']) @require_auth def do_travel(session_id: str): """Travel to a new location - returns updated DM response.""" client = get_api_client() location_id = request.form.get('location_id') if not location_id: return '
No destination selected.
', 400 try: response = client.post('/api/v1/travel', { 'session_id': session_id, 'location_id': location_id }) result = response.get('result', {}) # Check if travel triggers a job (narrative generation) job_id = result.get('job_id') if job_id: return render_template( 'dev/partials/job_status.html', job_id=job_id, session_id=session_id, status='queued' ) # Immediate response narrative = result.get('narrative', result.get('description', 'You arrive at your destination.')) location_name = result.get('location_name', 'Unknown Location') # Return script to close modal and update response return f'''
Arrived at {location_name}

{narrative}
''' except APIError as e: logger.error("failed_to_travel", session_id=session_id, location_id=location_id, error=str(e)) return f'
Travel failed: {e}
', 500 # ===== Combat Test Endpoints ===== @dev_bp.route('/combat') @require_auth def combat_hub(): """Combat testing hub - select character and enemies to start combat.""" client = get_api_client() try: # Get user's characters characters_response = client.get('/api/v1/characters') result = characters_response.get('result', {}) characters = result.get('characters', []) # Get available enemy templates enemies = [] try: enemies_response = client.get('/api/v1/combat/enemies') enemies = enemies_response.get('result', {}).get('enemies', []) except (APINotFoundError, APIError): # Enemies endpoint may not exist yet pass # Get all sessions to map characters to their sessions sessions_in_combat = [] character_session_map = {} # character_id -> session_id try: sessions_response = client.get('/api/v1/sessions') all_sessions = sessions_response.get('result', []) for session in all_sessions: # Map character to session (for dropdown) char_id = session.get('character_id') if char_id: character_session_map[char_id] = session.get('session_id') # Track sessions in combat (for resume list) if session.get('in_combat') or session.get('game_state', {}).get('in_combat'): sessions_in_combat.append(session) except (APINotFoundError, APIError): pass # Add session_id to each character for the template for char in characters: char['session_id'] = character_session_map.get(char.get('character_id')) return render_template( 'dev/combat.html', characters=characters, enemies=enemies, sessions_in_combat=sessions_in_combat ) except APIError as e: logger.error("failed_to_load_combat_hub", error=str(e)) return render_template('dev/combat.html', characters=[], enemies=[], sessions_in_combat=[], error=str(e)) @dev_bp.route('/combat/start', methods=['POST']) @require_auth def start_combat(): """Start a new combat encounter - returns redirect to combat session.""" client = get_api_client() session_id = request.form.get('session_id') enemy_ids = request.form.getlist('enemy_ids') logger.info("start_combat called", session_id=session_id, enemy_ids=enemy_ids, form_data=dict(request.form)) if not session_id: return '
No session selected
', 400 if not enemy_ids: return '
No enemies selected
', 400 try: response = client.post('/api/v1/combat/start', { 'session_id': session_id, 'enemy_ids': enemy_ids }) result = response.get('result', {}) # Return redirect script to combat session page return f'''
Combat started! Redirecting...
''' except APIError as e: logger.error("failed_to_start_combat", session_id=session_id, error=str(e)) return f'
Failed to start combat: {e}
', 500 @dev_bp.route('/combat/session/') @require_auth def combat_session(session_id: str): """Combat session debug interface - full 3-column layout.""" client = get_api_client() try: # Get combat state from API response = client.get(f'/api/v1/combat/{session_id}/state') result = response.get('result', {}) # Check if combat is still active if not result.get('in_combat'): # Combat ended - redirect to combat index return render_template('dev/combat.html', message="Combat has ended. Start a new combat to continue.") encounter = result.get('encounter') or {} combat_log = result.get('combat_log', []) # Get current turn combatant ID directly from API response current_turn_id = encounter.get('current_turn') turn_order = encounter.get('turn_order', []) # Find player and determine if it's player's turn is_player_turn = False player_combatant = None enemy_combatants = [] for combatant in encounter.get('combatants', []): if combatant.get('is_player'): player_combatant = combatant if combatant.get('combatant_id') == current_turn_id: is_player_turn = True else: enemy_combatants.append(combatant) # Format combat log entries for display formatted_log = [] for entry in combat_log: log_entry = { 'actor': entry.get('combatant_name', entry.get('actor', '')), 'message': entry.get('message', ''), 'damage': entry.get('damage'), 'heal': entry.get('healing'), 'is_crit': entry.get('is_critical', False), 'type': 'player' if entry.get('is_player', False) else 'enemy' } # Detect system messages if entry.get('action_type') in ('round_start', 'combat_start', 'combat_end'): log_entry['type'] = 'system' formatted_log.append(log_entry) return render_template( 'dev/combat_session.html', session_id=session_id, encounter=encounter, combat_log=formatted_log, current_turn_id=current_turn_id, is_player_turn=is_player_turn, player_combatant=player_combatant, enemy_combatants=enemy_combatants, turn_order=turn_order, raw_state=result ) except APINotFoundError: logger.warning("combat_not_found", session_id=session_id) return render_template('dev/combat.html', error=f"No active combat for session {session_id}"), 404 except APIError as e: logger.error("failed_to_load_combat_session", session_id=session_id, error=str(e)) return render_template('dev/combat.html', error=str(e)), 500 @dev_bp.route('/combat//state') @require_auth def combat_state(session_id: str): """Get combat state partial - returns refreshable state panel.""" client = get_api_client() try: response = client.get(f'/api/v1/combat/{session_id}/state') result = response.get('result', {}) # Check if combat is still active if not result.get('in_combat'): return '

Combat Ended

No active combat.

' encounter = result.get('encounter') or {} # Get current turn combatant ID directly from API response current_turn_id = encounter.get('current_turn') # Separate player and enemies player_combatant = None enemy_combatants = [] is_player_turn = False for combatant in encounter.get('combatants', []): if combatant.get('is_player'): player_combatant = combatant if combatant.get('combatant_id') == current_turn_id: is_player_turn = True else: enemy_combatants.append(combatant) return render_template( 'dev/partials/combat_state.html', session_id=session_id, encounter=encounter, player_combatant=player_combatant, enemy_combatants=enemy_combatants, current_turn_id=current_turn_id, is_player_turn=is_player_turn ) except APIError as e: logger.error("failed_to_get_combat_state", session_id=session_id, error=str(e)) return f'
Failed to load state: {e}
', 500 @dev_bp.route('/combat//action', methods=['POST']) @require_auth def combat_action(session_id: str): """Execute a combat action - returns log entry HTML.""" client = get_api_client() action_type = request.form.get('action_type', 'attack') ability_id = request.form.get('ability_id') item_id = request.form.get('item_id') target_id = request.form.get('target_id') try: payload = {'action_type': action_type} if ability_id: payload['ability_id'] = ability_id if item_id: payload['item_id'] = item_id if target_id: payload['target_id'] = target_id response = client.post(f'/api/v1/combat/{session_id}/action', payload) result = response.get('result', {}) # Check if combat ended combat_ended = result.get('combat_ended', False) combat_status = result.get('combat_status') if combat_ended: # API returns lowercase status values: 'victory', 'defeat', 'fled' status_lower = (combat_status or '').lower() if status_lower == 'victory': return render_template( 'dev/partials/combat_victory.html', session_id=session_id, rewards=result.get('rewards', {}) ) elif status_lower == 'defeat': return render_template( 'dev/partials/combat_defeat.html', session_id=session_id, gold_lost=result.get('gold_lost', 0) ) # Format action result for log # API returns data directly in result, not nested under 'action_result' log_entries = [] player_entry = { 'actor': 'You', 'message': result.get('message', f'used {action_type}'), 'type': 'player', 'is_crit': False } damage_results = result.get('damage_results', []) if damage_results: for dmg in damage_results: player_entry['damage'] = dmg.get('total_damage') or dmg.get('damage') player_entry['is_crit'] = dmg.get('is_critical', False) if player_entry['is_crit']: player_entry['type'] = 'crit' if result.get('healing'): player_entry['heal'] = result.get('healing') player_entry['type'] = 'heal' log_entries.append(player_entry) for effect in result.get('effects_applied', []): log_entries.append({ 'actor': '', 'message': effect.get('message', f'Effect applied: {effect.get("name")}'), 'type': 'system' }) # Return log entries with optional enemy turn trigger from flask import make_response resp = make_response(render_template( 'dev/partials/combat_debug_log.html', combat_log=log_entries )) # Trigger enemy turn if needed if result.get('next_combatant_id') and not result.get('next_is_player', True): resp.headers['HX-Trigger'] = 'enemyTurn' return resp except APIError as e: logger.error("combat_action_failed", session_id=session_id, action_type=action_type, error=str(e)) return f'''
Action failed: {e}
''', 500 @dev_bp.route('/combat//enemy-turn', methods=['POST']) @require_auth def combat_enemy_turn(session_id: str): """Execute enemy turn - returns log entry HTML.""" client = get_api_client() try: response = client.post(f'/api/v1/combat/{session_id}/enemy-turn', {}) result = response.get('result', {}) # Check if combat ended combat_ended = result.get('combat_ended', False) combat_status = result.get('combat_status') if combat_ended: # API returns lowercase status values: 'victory', 'defeat', 'fled' status_lower = (combat_status or '').lower() if status_lower == 'victory': return render_template( 'dev/partials/combat_victory.html', session_id=session_id, rewards=result.get('rewards', {}) ) elif status_lower == 'defeat': return render_template( 'dev/partials/combat_defeat.html', session_id=session_id, gold_lost=result.get('gold_lost', 0) ) # Format enemy action for log # The API returns the action result directly with a complete message damage_results = result.get('damage_results', []) is_crit = damage_results[0].get('is_critical', False) if damage_results else False log_entries = [{ 'actor': '', # Message already contains the actor name 'message': result.get('message', 'Enemy attacks!'), 'type': 'crit' if is_crit else 'enemy', 'is_crit': is_crit, 'damage': damage_results[0].get('total_damage') if damage_results else None }] from flask import make_response resp = make_response(render_template( 'dev/partials/combat_debug_log.html', combat_log=log_entries )) # Trigger another enemy turn if needed if result.get('next_combatant_id') and not result.get('next_is_player', True): resp.headers['HX-Trigger'] = 'enemyTurn' return resp except APIError as e: logger.error("enemy_turn_failed", session_id=session_id, error=str(e)) return f'''
Enemy turn error: {e}
''', 500 @dev_bp.route('/combat//abilities') @require_auth def combat_abilities(session_id: str): """Get abilities modal for combat.""" client = get_api_client() try: response = client.get(f'/api/v1/combat/{session_id}/state') result = response.get('result', {}) encounter = result.get('encounter', {}) player_combatant = None for combatant in encounter.get('combatants', []): if combatant.get('is_player'): player_combatant = combatant break abilities = [] if player_combatant: ability_ids = player_combatant.get('abilities', []) current_mp = player_combatant.get('current_mp', 0) cooldowns = player_combatant.get('cooldowns', {}) for ability_id in ability_ids: try: ability_response = client.get(f'/api/v1/abilities/{ability_id}') ability_data = ability_response.get('result', {}) mp_cost = ability_data.get('mp_cost', 0) cooldown = cooldowns.get(ability_id, 0) available = current_mp >= mp_cost and cooldown == 0 abilities.append({ 'id': ability_id, 'name': ability_data.get('name', ability_id), 'description': ability_data.get('description', ''), 'mp_cost': mp_cost, 'cooldown': cooldown, 'max_cooldown': ability_data.get('cooldown', 0), 'damage_type': ability_data.get('damage_type'), 'effect_type': ability_data.get('effect_type'), 'available': available }) except (APINotFoundError, APIError): abilities.append({ 'id': ability_id, 'name': ability_id.replace('_', ' ').title(), 'description': '', 'mp_cost': 0, 'cooldown': cooldowns.get(ability_id, 0), 'max_cooldown': 0, 'available': True }) return render_template( 'dev/partials/ability_modal.html', session_id=session_id, abilities=abilities ) except APIError as e: logger.error("failed_to_load_abilities", session_id=session_id, error=str(e)) return f''' ''' @dev_bp.route('/combat//items') @require_auth def combat_items(session_id: str): """Get combat items bottom sheet (consumables only).""" client = get_api_client() try: session_response = client.get(f'/api/v1/sessions/{session_id}') session_data = session_response.get('result', {}) character_id = session_data.get('character_id') consumables = [] if character_id: try: inv_response = client.get(f'/api/v1/characters/{character_id}/inventory') inv_data = inv_response.get('result', {}) inventory = inv_data.get('inventory', []) for item in inventory: item_type = item.get('item_type', item.get('type', '')) if item_type == 'consumable' or item.get('usable_in_combat', False): consumables.append({ 'item_id': item.get('item_id'), 'name': item.get('name', 'Unknown Item'), 'description': item.get('description', ''), 'effects_on_use': item.get('effects_on_use', []), 'rarity': item.get('rarity', 'common') }) except (APINotFoundError, APIError) as e: logger.warning("failed_to_load_combat_inventory", character_id=character_id, error=str(e)) return render_template( 'dev/partials/combat_items_sheet.html', session_id=session_id, consumables=consumables, has_consumables=len(consumables) > 0 ) except APIError as e: logger.error("failed_to_load_items", session_id=session_id, error=str(e)) return f'''

Use Item

Failed to load items: {e}
''' @dev_bp.route('/combat//items//detail') @require_auth def combat_item_detail(session_id: str, item_id: str): """Get item detail for combat bottom sheet.""" client = get_api_client() try: session_response = client.get(f'/api/v1/sessions/{session_id}') session_data = session_response.get('result', {}) character_id = session_data.get('character_id') item = None if character_id: inv_response = client.get(f'/api/v1/characters/{character_id}/inventory') inv_data = inv_response.get('result', {}) inventory = inv_data.get('inventory', []) for inv_item in inventory: if inv_item.get('item_id') == item_id: item = inv_item break if not item: return '

Item not found

', 404 effect_desc = item.get('description', 'Use this item') effects = item.get('effects_on_use', []) if effects: effect_parts = [] for effect in effects: if effect.get('stat') == 'hp': effect_parts.append(f"Restores {effect.get('value', 0)} HP") elif effect.get('stat') == 'mp': effect_parts.append(f"Restores {effect.get('value', 0)} MP") elif effect.get('name'): effect_parts.append(effect.get('name')) if effect_parts: effect_desc = ', '.join(effect_parts) return f'''
{item.get('name', 'Item')}
{effect_desc}
''' except APIError as e: logger.error("failed_to_load_combat_item_detail", session_id=session_id, item_id=item_id, error=str(e)) return f'

Failed to load item: {e}

', 500 @dev_bp.route('/combat//end', methods=['POST']) @require_auth def force_end_combat(session_id: str): """Force end combat (debug action).""" client = get_api_client() victory = request.form.get('victory', 'true').lower() == 'true' try: response = client.post(f'/api/v1/combat/{session_id}/end', {'victory': victory}) result = response.get('result', {}) if victory: return render_template( 'dev/partials/combat_victory.html', session_id=session_id, rewards=result.get('rewards', {}) ) else: return render_template( 'dev/partials/combat_defeat.html', session_id=session_id, gold_lost=result.get('gold_lost', 0) ) except APIError as e: logger.error("failed_to_end_combat", session_id=session_id, error=str(e)) return f'
Failed to end combat: {e}
', 500 @dev_bp.route('/combat//reset-hp-mp', methods=['POST']) @require_auth def reset_hp_mp(session_id: str): """Reset player HP and MP to full (debug action).""" client = get_api_client() try: response = client.post(f'/api/v1/combat/{session_id}/debug/reset-hp-mp', {}) result = response.get('result', {}) return f'''
HP and MP restored to full! (HP: {result.get('current_hp', '?')}/{result.get('max_hp', '?')}, MP: {result.get('current_mp', '?')}/{result.get('max_mp', '?')})
''' except APIError as e: logger.error("failed_to_reset_hp_mp", session_id=session_id, error=str(e)) return f'''
Failed to reset HP/MP: {e}
''', 500 @dev_bp.route('/combat//log') @require_auth def combat_log(session_id: str): """Get full combat log.""" client = get_api_client() try: response = client.get(f'/api/v1/combat/{session_id}/state') result = response.get('result', {}) combat_log_data = result.get('combat_log', []) formatted_log = [] for entry in combat_log_data: log_entry = { 'actor': entry.get('combatant_name', entry.get('actor', '')), 'message': entry.get('message', ''), 'damage': entry.get('damage'), 'heal': entry.get('healing'), 'is_crit': entry.get('is_critical', False), 'type': 'player' if entry.get('is_player', False) else 'enemy' } if entry.get('action_type') in ('round_start', 'combat_start', 'combat_end'): log_entry['type'] = 'system' formatted_log.append(log_entry) return render_template( 'dev/partials/combat_debug_log.html', combat_log=formatted_log ) except APIError as e: logger.error("failed_to_load_combat_log", session_id=session_id, error=str(e)) return '
Failed to load combat log
', 500