import os from datetime import datetime from sqlalchemy import text from flask import Flask from flask_login import LoginManager from werkzeug.security import generate_password_hash from web.db import db, User, Run from web.tasks import huey from core import config # Calculate paths relative to this file (web/app.py -> project root is two levels up) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) TEMPLATE_DIR = os.path.join(BASE_DIR, 'templates') app = Flask(__name__, template_folder=TEMPLATE_DIR) app.url_map.strict_slashes = False app.config['SECRET_KEY'] = config.FLASK_SECRET app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{os.path.join(config.DATA_DIR, "bookapp.db")}' db.init_app(app) login_manager = LoginManager() login_manager.login_view = 'auth.login' login_manager.init_app(app) @login_manager.user_loader def load_user(user_id): return db.session.get(User, int(user_id)) @app.context_processor def inject_globals(): return dict(app_version=config.VERSION) # Register Blueprints from web.routes.auth import auth_bp from web.routes.project import project_bp from web.routes.run import run_bp from web.routes.persona import persona_bp from web.routes.admin import admin_bp app.register_blueprint(auth_bp) app.register_blueprint(project_bp) app.register_blueprint(run_bp) app.register_blueprint(persona_bp) app.register_blueprint(admin_bp) # --- SETUP --- with app.app_context(): db.create_all() # Auto-create Admin from Environment Variables (Docker/Portainer Setup) if config.ADMIN_USER and config.ADMIN_PASSWORD: admin = User.query.filter_by(username=config.ADMIN_USER).first() if not admin: print(f"🔐 System: Creating Admin User '{config.ADMIN_USER}' from environment variables.") admin = User(username=config.ADMIN_USER, password=generate_password_hash(config.ADMIN_PASSWORD, method='pbkdf2:sha256'), is_admin=True) db.session.add(admin) db.session.commit() else: print(f"🔐 System: Syncing Admin User '{config.ADMIN_USER}' settings from environment.") if not admin.is_admin: admin.is_admin = True admin.password = generate_password_hash(config.ADMIN_PASSWORD, method='pbkdf2:sha256') db.session.add(admin) db.session.commit() elif not User.query.filter_by(is_admin=True).first(): print("â„šī¸ System: No Admin credentials found in environment variables. Admin account not created.") # Migration: Add 'progress' column if missing try: with db.engine.connect() as conn: conn.execute(text("ALTER TABLE run ADD COLUMN progress INTEGER DEFAULT 0")) conn.commit() print("✅ System: Added 'progress' column to Run table.") except: pass # Reset stuck runs on startup try: stuck_runs = Run.query.filter_by(status='running').all() if stuck_runs: print(f"âš ī¸ System: Found {len(stuck_runs)} stuck runs. Resetting to 'failed'.") for r in stuck_runs: r.status = 'failed' r.end_time = datetime.utcnow() db.session.commit() except Exception as e: print(f"âš ī¸ System: Failed to clean up stuck runs: {e}") # --- HUEY CONSUMER --- # Start the Huey task consumer in a background thread whenever the app loads. # Guard against the Werkzeug reloader spawning a second consumer in the child process, # and against test runners or importers that should not start background workers. import threading as _threading def _start_huey_consumer(): try: from huey.consumer import Consumer consumer = Consumer(huey, workers=1, worker_type='thread', loglevel=20) print("✅ System: Huey task consumer started.") consumer.run() except Exception as e: print(f"âš ī¸ System: Huey consumer failed to start: {e}") _is_reloader_child = os.environ.get('WERKZEUG_RUN_MAIN') == 'true' _is_testing = os.environ.get('FLASK_TESTING') == '1' if not _is_reloader_child and not _is_testing: _huey_thread = _threading.Thread(target=_start_huey_consumer, daemon=True, name="huey-consumer") _huey_thread.start() if __name__ == "__main__": app.run(host='0.0.0.0', port=5000, debug=False)