"""BalanceBoard Flask application factory.""" import logging import time from flask import Flask, redirect, render_template, request, url_for from flask_login import current_user from markupsafe import Markup, escape from blueprints.api import create_api_blueprint from config import Config, DEFAULT_PORT from database import db, init_db from extensions import get_filter_engine, get_polling_service, login_manager, oauth from models import User, bcrypt from routes.admin import register_admin_routes from routes.assets import register_asset_routes from routes.auth import register_auth_routes from routes.pages import register_page_routes from routes.settings import register_settings_routes from user_service import UserService logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", handlers=[logging.FileHandler("app.log"), logging.StreamHandler()], ) logger = logging.getLogger(__name__) def create_app(config_class=Config): """Create and configure the Flask application.""" app = Flask(__name__, static_folder="themes", template_folder="templates") app.config.from_object(config_class) init_db(app) bcrypt.init_app(app) login_manager.init_app(app) oauth.init_app(app) user_service = UserService() services_started = {"value": False} @app.before_request def _ensure_services_started(): """Initialize background services lazily on the first request.""" if services_started["value"]: return filter_engine = get_filter_engine() logger.info( "FilterEngine initialized with %s filtersets", len(filter_engine.get_available_filtersets()), ) polling_service = get_polling_service() polling_service.init_app(app) polling_service.start() services_started["value"] = True auth0 = oauth.register( "auth0", client_id=app.config["AUTH0_CLIENT_ID"], client_secret=app.config["AUTH0_CLIENT_SECRET"], server_metadata_url=f"https://{app.config['AUTH0_DOMAIN']}/.well-known/openid_configuration", client_kwargs={"scope": "openid profile email"}, ) app.register_blueprint(create_api_blueprint(), url_prefix="/api/v1") register_asset_routes(app) register_page_routes(app) register_settings_routes(app, user_service) register_admin_routes(app, user_service) register_auth_routes(app, user_service, auth0) register_template_filters(app) register_login_loader(user_service) register_request_guards(app) register_context_processors(app) register_error_handlers(app) return app def register_template_filters(app): """Register Jinja filters used by legacy templates.""" @app.template_filter("nl2br") def nl2br_filter(text): """Convert newlines to escaped
tags.""" if not text: return text return Markup(str(escape(text)).replace("\n", "
\n")) @app.template_filter("timeago") def timeago_filter(timestamp): """Format a unix timestamp as a relative age string.""" if not timestamp: return "Recently" try: ts = float(timestamp) except (TypeError, ValueError): return "Recently" diff = time.time() - ts if diff < 0: return "Recently" if diff < 3600: return f"{int(diff // 60)}m ago" if diff < 86400: return f"{int(diff // 3600)}h ago" if diff < 604800: return f"{int(diff // 86400)}d ago" return time.strftime("%Y-%m-%d", time.localtime(ts)) def register_login_loader(user_service): """Register Flask-Login user loading.""" @login_manager.user_loader def load_user(user_id): return user_service.get_user_by_id(user_id) def register_request_guards(app): """Register request guards shared by page routes.""" @app.before_request def check_first_user(): """Redirect to admin setup if no users exist yet.""" if request.endpoint and ( request.endpoint.startswith("static") or request.endpoint in ["login", "signup", "admin_setup", "serve_theme", "serve_logo"] ): return if current_user.is_authenticated: return try: if User.query.count() == 0: return redirect(url_for("admin_setup")) except Exception as e: logger.warning(f"Database not ready for user count check: {e}") def register_context_processors(app): """Register template context processors.""" @app.context_processor def inject_app_config(): return {"APP_NAME": app.config["APP_NAME"]} def register_error_handlers(app): """Register error pages for the legacy Jinja UI.""" @app.errorhandler(404) def not_found(e): return render_template("404.html"), 404 @app.errorhandler(500) def server_error(e): return render_template("500.html"), 500 if __name__ == "__main__": flask_app = create_app() print("BalanceBoard starting...") print("Database: PostgreSQL with SQLAlchemy") print("Password hashing: bcrypt") print("Authentication: Flask-Login") flask_app.run(host="0.0.0.0", port=DEFAULT_PORT, debug=True)