Root cause: Consumer(huey, workers=1, worker_type='thread', loglevel=20) raised TypeError on every app start because Huey 2.6.0 does not accept a `loglevel` keyword argument. The exception was silently caught and only printed to stdout, so the consumer never ran and all tasks stayed 'queued' forever — causing the 'Preparing environment / Waiting for logs' hang. Fixes: - web/app.py: Remove invalid `loglevel=20` from Consumer(); configure Huey logging via logging.basicConfig(WARNING) instead. Add persistent error logging to data/consumer_error.log for future diagnosis. - core/config.py: Replace emoji print() calls with ASCII-safe equivalents to prevent UnicodeEncodeError on Windows cp1252 terminals at import time. - core/config.py: Update VERSION to 2.9 (was stale at 1.5.0). - ai_blueprint.md: Bump to v2.10, document root cause and fixes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
131 lines
4.7 KiB
Python
131 lines
4.7 KiB
Python
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():
|
||
import logging as _logging
|
||
_logging.basicConfig(level=_logging.WARNING)
|
||
try:
|
||
from huey.consumer import Consumer
|
||
consumer = Consumer(huey, workers=1, worker_type='thread')
|
||
print("System: Huey task consumer started.")
|
||
consumer.run()
|
||
except Exception as e:
|
||
msg = f"System: Huey consumer failed to start: {e}"
|
||
print(msg)
|
||
# Write the error to a persistent log so it is visible even when stdout is lost
|
||
try:
|
||
import os as _os
|
||
from core import config as _cfg
|
||
_err_path = _os.path.join(_cfg.DATA_DIR, "consumer_error.log")
|
||
with open(_err_path, 'a', encoding='utf-8') as _f:
|
||
import datetime as _dt
|
||
_f.write(f"[{_dt.datetime.now()}] {msg}\n")
|
||
except Exception:
|
||
pass
|
||
|
||
_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)
|