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