""" 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