Replaced monolithic modules/ package with a clean architecture:
- core/ config.py, utils.py
- ai/ models.py (ResilientModel), setup.py (init_models)
- story/ planner.py, writer.py, editor.py, style_persona.py, bible_tracker.py
- marketing/ cover.py, blurb.py, fonts.py, assets.py
- export/ exporter.py
- web/ app.py (Flask factory), db.py, helpers.py, tasks.py, routes/{auth,project,run,persona,admin}.py
- cli/ engine.py (run_generation), wizard.py (BookWizard)
Flask routes split into 5 Blueprints; all templates updated with blueprint-
prefixed url_for() calls. Dockerfile and docker-compose updated to use
web.app entry point and new package paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
107 lines
3.6 KiB
Python
107 lines
3.6 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}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
import threading
|
||
from huey.contrib.mini import MiniHuey
|
||
|
||
# Start Huey consumer in background thread
|
||
def run_huey():
|
||
from huey.consumer import Consumer
|
||
consumer = Consumer(huey, workers=1, worker_type='thread', loglevel=20)
|
||
consumer.run()
|
||
|
||
t = threading.Thread(target=run_huey, daemon=True)
|
||
t.start()
|
||
|
||
app.run(host='0.0.0.0', port=7070, debug=False)
|