import logging import os import threading import time from sqlalchemy import inspect from sqlalchemy.engine.url import make_url from app import app, db from config import PORT from werkzeug.serving import WSGIRequestHandler from logging_utils import cleanup_old_logs # --- Alembic migration runner --- from alembic import command from alembic.config import Config # On startup, run Alembic migrations to ensure DB schema is up to date. # To generate a migration after changing models: # 1. Run: alembic -c migrations/alembic.ini revision --autogenerate -m "Describe your change". To upstream: alembic -c migrations/alembic.ini upgrade head # 2. Review the migration file in migrations/versions/ # 3. (Optional) Edit for correctness # 4. Migrations will be applied automatically on next startup def run_migrations(): alembic_cfg = Config("migrations/alembic.ini") with app.app_context(): command.upgrade(alembic_cfg, "head") def ensure_sqlite_database(): """Bootstrap SQLite DB files before Alembic runs.""" db_uri = app.config.get("SQLALCHEMY_DATABASE_URI", "") if not db_uri.startswith("sqlite"): return url = make_url(db_uri) database = url.database if not database: return if os.path.isabs(database): db_path = database else: db_path = os.path.join(app.instance_path, database) os.makedirs(os.path.dirname(db_path), exist_ok=True) bootstrap_reason = None if not os.path.exists(db_path): bootstrap_reason = "file missing" else: with app.app_context(): inspector = inspect(db.engine) tables = inspector.get_table_names() if 'users' not in tables: bootstrap_reason = "schema missing" if not bootstrap_reason: logging.info(f"SQLite database already initialized at {db_path}") return logging.info(f"Bootstrapping SQLite database at {db_path} ({bootstrap_reason})") with app.app_context(): db.create_all() logging.info("SQLite database initialization complete") def schedule_log_cleanup(): """ Background thread that runs log cleanup daily. Cleans up logs older than LOG_RETENTION_DAYS (default: 10 days). """ def cleanup_thread(): while True: try: # Wait 24 hours between cleanups time.sleep(24 * 60 * 60) with app.app_context(): deleted = cleanup_old_logs() if deleted > 0: logging.info(f"Scheduled cleanup: removed {deleted} old log entries") except Exception as e: logging.error(f"Error in scheduled log cleanup: {e}") # Start cleanup thread as daemon so it exits when main thread exits thread = threading.Thread(target=cleanup_thread, daemon=True) thread.start() logging.info("Log cleanup scheduler started (runs daily)") # Filter out specific paths from Werkzeug logs class FilteredWSGIRequestHandler(WSGIRequestHandler): def log(self, type, message, *args): # Skip logging for Chrome DevTools discovery requests if '.well-known/appspecific/com.chrome.devtools.json' in message: return return super().log(type, message, *args) if __name__ == '__main__': from config import create_default_admin_user ensure_sqlite_database() run_migrations() # Always ensure DB schema is up to date before serving with app.app_context(): create_default_admin_user(app) # Run initial cleanup on startup cleanup_old_logs() # Start background log cleanup scheduler schedule_log_cleanup() # Use filtered request handler debug_mode = os.environ.get('FLASK_DEBUG', 'false').lower() in ('true', '1', 'yes') app.run(host='0.0.0.0', port=PORT, debug=debug_mode, request_handler=FilteredWSGIRequestHandler)