""" Public Web Frontend - Flask Application Factory This is a lightweight web frontend that provides HTML/HTMX UI for the Code of Conquest game. All business logic is handled by the API backend - this frontend only renders views and makes HTTP requests to the API. """ from flask import Flask from flask import render_template import structlog import yaml import os from pathlib import Path from datetime import datetime, timezone logger = structlog.get_logger(__name__) def load_config(): """Load configuration from YAML file based on environment.""" env = os.getenv("FLASK_ENV", "development") config_path = Path(__file__).parent.parent / "config" / f"{env}.yaml" with open(config_path, 'r') as f: config = yaml.safe_load(f) logger.info("configuration_loaded", env=env, config_path=str(config_path)) return config def create_app(): """Create and configure the Flask application.""" app = Flask(__name__, template_folder="../templates", static_folder="../static") # Load configuration config = load_config() app.config.update(config) # Configure secret key from environment app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') # Context processor to make API config and user available in templates @app.context_processor def inject_template_globals(): """Make API base URL and current user available to all templates.""" from .utils.auth import get_current_user return { 'api_base_url': app.config.get('api', {}).get('base_url', 'http://localhost:5000'), 'current_user': get_current_user() } # Register blueprints from .views.auth_views import auth_bp from .views.character_views import character_bp from .views.game_views import game_bp from .views.pages import pages_bp app.register_blueprint(auth_bp) app.register_blueprint(character_bp) app.register_blueprint(game_bp) app.register_blueprint(pages_bp) # Register Jinja filters def format_timestamp(iso_string: str) -> str: """Convert ISO timestamp to relative time (e.g., '2 mins ago')""" if not iso_string: return "" try: timestamp = datetime.fromisoformat(iso_string.replace('Z', '+00:00')) now = datetime.now(timezone.utc) diff = now - timestamp seconds = diff.total_seconds() if seconds < 60: return "Just now" elif seconds < 3600: mins = int(seconds / 60) return f"{mins} min{'s' if mins != 1 else ''} ago" elif seconds < 86400: hours = int(seconds / 3600) return f"{hours} hr{'s' if hours != 1 else ''} ago" else: days = int(seconds / 86400) return f"{days} day{'s' if days != 1 else ''} ago" except Exception as e: logger.warning("timestamp_format_failed", iso_string=iso_string, error=str(e)) return iso_string app.jinja_env.filters['format_timestamp'] = format_timestamp # Register dev blueprint only in development env = os.getenv("FLASK_ENV", "development") if env == "development": from .views.dev import dev_bp app.register_blueprint(dev_bp) logger.info("dev_blueprint_registered", message="Dev testing routes available at /dev") # Error handlers @app.errorhandler(404) def not_found(error): return render_template('errors/404.html'), 404 @app.errorhandler(500) def internal_error(error): logger.error("internal_server_error", error=str(error)) return render_template('errors/500.html'), 500 logger.info("flask_app_created", blueprints=["auth", "character", "game", "pages"]) return app