Files
bookapp/web/app.py
Mike Wichers 0d4b9b761b Auto-commit: v2.10 — Docker diagnostic logging for consumer & task execution
- web/app.py: Startup banner to docker logs (Python version, platform,
  Huey version, DB paths). All print() calls now flush=True so Docker
  captures them immediately. Emoji-free for robust stdout encoding.
  Startup now detects orphaned queued runs (queue empty but DB queued)
  and resets them to 'failed' so the UI does not stay stuck on reload.
  Huey logging configured at INFO level so task pick-up/completion
  appears in `docker logs`. Consumer skip reason logged explicitly.
- web/tasks.py: generate_book_task now emits [TASK run=N] lines to
  stdout (docker logs) at pick-up, log-file creation, DB status update,
  and on error (with full traceback) so failures are always visible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 12:05:07 -05:00

182 lines
6.9 KiB
Python

import os
import sys
import platform
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
# Ensure stdout is UTF-8 in all environments (Docker, Windows, Raspberry Pi)
if hasattr(sys.stdout, 'reconfigure'):
try:
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
except Exception:
pass
def _log(msg):
"""Print to stdout with flush so Docker logs capture it immediately."""
print(msg, flush=True)
# 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)
# --- STARTUP DIAGNOSTIC BANNER ---
_log("=" * 60)
_log(f"BookApp v{config.VERSION} starting up")
_log(f" Python : {sys.version}")
_log(f" Platform : {platform.platform()}")
_log(f" Data dir : {config.DATA_DIR}")
_log(f" Queue db : {os.path.join(config.DATA_DIR, 'queue.db')}")
_log(f" App db : {os.path.join(config.DATA_DIR, 'bookapp.db')}")
try:
import huey as _huey_pkg
_log(f" Huey : {_huey_pkg.__version__}")
except Exception:
_log(" Huey : (version unknown)")
_log("=" * 60)
# --- 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:
_log(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:
_log(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():
_log("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()
_log("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:
_log(f"System: Found {len(stuck_runs)} stuck run(s) — resetting to 'failed'.")
for r in stuck_runs:
r.status = 'failed'
r.end_time = datetime.utcnow()
db.session.commit()
# Also reset stuck 'queued' runs whose task entry was lost from queue.db
import sqlite3 as _sqlite3
_queue_path = os.path.join(config.DATA_DIR, 'queue.db')
if os.path.exists(_queue_path):
with _sqlite3.connect(_queue_path, timeout=5) as _qconn:
pending_count = _qconn.execute("SELECT COUNT(*) FROM task").fetchone()[0]
queued_runs = Run.query.filter_by(status='queued').count()
_log(f"System: Queue has {pending_count} pending task(s), DB has {queued_runs} queued run(s).")
if queued_runs > 0 and pending_count == 0:
_log("System: WARNING — queued runs exist but queue is empty (tasks lost). Resetting to 'failed'.")
Run.query.filter_by(status='queued').update({'status': 'failed', 'end_time': datetime.utcnow()})
db.session.commit()
except Exception as e:
_log(f"System: Startup cleanup error: {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
# INFO level so task pick-up/completion appears in docker logs
_logging.basicConfig(
level=_logging.INFO,
format='[%(asctime)s] HUEY %(levelname)s | %(message)s',
datefmt='%H:%M:%S',
stream=sys.stdout,
force=True,
)
try:
from huey.consumer import Consumer
# NOTE: Huey 2.6.0 does NOT accept a `loglevel` kwarg — omit it.
consumer = Consumer(huey, workers=1, worker_type='thread')
_log("System: Huey task consumer started successfully.")
consumer.run() # blocks until app exits
except Exception as e:
msg = f"System: Huey consumer FAILED to start: {type(e).__name__}: {e}"
_log(msg)
# Also write to a persistent file for diagnosis when stdout is piped away
try:
_err_path = os.path.join(config.DATA_DIR, "consumer_error.log")
with open(_err_path, 'a', encoding='utf-8') as _f:
_f.write(f"[{datetime.utcnow().isoformat()}] {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:
_log("System: Launching Huey consumer thread...")
_huey_thread = _threading.Thread(target=_start_huey_consumer, daemon=True, name="huey-consumer")
_huey_thread.start()
else:
_log(f"System: Skipping Huey consumer (WERKZEUG_RUN_MAIN={os.environ.get('WERKZEUG_RUN_MAIN')}, FLASK_TESTING={os.environ.get('FLASK_TESTING')}).")
if __name__ == "__main__":
app.run(host='0.0.0.0', port=5000, debug=False)