Initial commit: Leopost Full — merge di Leopost, Post Generator e Autopilot OS
- Backend FastAPI con multi-LLM (Claude/OpenAI/Gemini) - Publishing su Facebook, Instagram, YouTube, TikTok - Calendario editoriale con awareness levels (PAS, AIDA, BAB...) - Design system Editorial Fresh (Fraunces + DM Sans) - Scheduler automatico, gestione commenti AI, affiliate links Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
156
backend/app/main.py
Normal file
156
backend/app/main.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""Leopost Full — FastAPI application.
|
||||
|
||||
IMPORTANT: root_path is intentionally NOT set in the FastAPI() constructor.
|
||||
It is passed only via Uvicorn's --root-path flag at runtime.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from .auth import hash_password
|
||||
from .auth import router as auth_router
|
||||
from .config import settings
|
||||
from .database import Base, SessionLocal, engine
|
||||
from .models import User
|
||||
from .routers.affiliates import router as affiliates_router
|
||||
from .routers.characters import router as characters_router
|
||||
from .routers.comments import router as comments_router
|
||||
from .routers.content import router as content_router
|
||||
from .routers.editorial import router as editorial_router
|
||||
from .routers.plans import router as plans_router
|
||||
from .routers.settings import router as settings_router
|
||||
from .routers.social import router as social_router
|
||||
|
||||
logger = logging.getLogger("leopost")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SPA static files with catch-all fallback
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class SPAStaticFiles(StaticFiles):
|
||||
"""Serve a React SPA: return index.html for any 404 so the client-side
|
||||
router handles unknown paths instead of returning a 404 from the server."""
|
||||
|
||||
async def get_response(self, path: str, scope):
|
||||
try:
|
||||
return await super().get_response(path, scope)
|
||||
except Exception:
|
||||
return await super().get_response("index.html", scope)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Background scheduler
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_scheduler_running = False
|
||||
|
||||
|
||||
def _scheduler_loop():
|
||||
"""Simple scheduler that checks for posts to publish every 60 seconds."""
|
||||
from .scheduler import check_and_publish
|
||||
|
||||
global _scheduler_running
|
||||
while _scheduler_running:
|
||||
try:
|
||||
check_and_publish()
|
||||
except Exception as e:
|
||||
logger.error(f"Scheduler error: {e}")
|
||||
sleep(60)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifespan
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
global _scheduler_running
|
||||
|
||||
# Ensure data directory exists
|
||||
data_dir = Path("./data")
|
||||
data_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create tables
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
# Create admin user if not exists
|
||||
db = SessionLocal()
|
||||
try:
|
||||
existing = db.query(User).filter(User.username == settings.admin_username).first()
|
||||
if not existing:
|
||||
admin = User(
|
||||
username=settings.admin_username,
|
||||
hashed_password=hash_password(settings.admin_password),
|
||||
)
|
||||
db.add(admin)
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# Start background scheduler
|
||||
_scheduler_running = True
|
||||
scheduler_thread = Thread(target=_scheduler_loop, daemon=True)
|
||||
scheduler_thread.start()
|
||||
logger.info("Background scheduler started")
|
||||
|
||||
yield
|
||||
|
||||
# Shutdown scheduler
|
||||
_scheduler_running = False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Application
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# CRITICAL: Do NOT pass root_path here — use Uvicorn --root-path instead.
|
||||
app = FastAPI(
|
||||
title="Leopost Full",
|
||||
version="0.1.0",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:5173"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# API routes (must be registered BEFORE the SPA catch-all mount)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
app.include_router(auth_router)
|
||||
app.include_router(characters_router)
|
||||
app.include_router(content_router)
|
||||
app.include_router(affiliates_router)
|
||||
app.include_router(plans_router)
|
||||
app.include_router(social_router)
|
||||
app.include_router(comments_router)
|
||||
app.include_router(settings_router)
|
||||
app.include_router(editorial_router)
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
def health():
|
||||
return {"status": "ok", "version": "0.1.0"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SPA catch-all mount (MUST be last)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_STATIC_DIR = Path(__file__).parent.parent.parent / "static"
|
||||
|
||||
if _STATIC_DIR.exists():
|
||||
app.mount("/", SPAStaticFiles(directory=str(_STATIC_DIR), html=True), name="spa")
|
||||
Reference in New Issue
Block a user