""" Forecasting Application — FastAPI Backend Auth is handled by the central HNF stack cookie (hnf_session). """ import logging import sys from contextlib import asynccontextmanager logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[logging.StreamHandler(sys.stdout)] ) from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from sqlalchemy import text from database import async_engine from api import ( forecast, sync, export, budget, accuracy, evolution, crossref, explain, config, historical, resos, backtest, sync_bookings, resos_sync, reports, special_dates, backup, public, bookability, competitor_rates, ai_insights, ) from scheduler import start_scheduler, shutdown_scheduler @asynccontextmanager async def lifespan(app: FastAPI): # Apply database schema (idempotent CREATE TABLE IF NOT EXISTS) # Uses psycopg2 directly — schema.sql contains PL/pgSQL with $1/$2 syntax # that SQLAlchemy text() misinterprets as bindparams. try: import os, psycopg2 from database import DATABASE_URL schema_path = os.path.join(os.path.dirname(__file__), 'schema.sql') if os.path.exists(schema_path): with open(schema_path) as f: sql = f.read() conn = psycopg2.connect(DATABASE_URL) try: conn.autocommit = True with conn.cursor() as cur: cur.execute(sql) logging.getLogger(__name__).info("Schema applied successfully") finally: conn.close() except Exception as e: logging.getLogger(__name__).warning(f"Schema init failed: {e}") # Startup: clean up stale scrape batches try: from services.booking_scraper import cleanup_stale_batches from database import SyncSessionLocal db = SyncSessionLocal() try: cleanup_stale_batches(db, max_age_minutes=10) finally: db.close() except Exception as e: logging.getLogger(__name__).warning(f"Stale batch cleanup failed: {e}") # Startup: mark any sync_log entries stuck as 'running' as failed # (these are orphaned by container restarts mid-job) try: import psycopg2 from database import DATABASE_URL conn = psycopg2.connect(DATABASE_URL) try: conn.autocommit = True with conn.cursor() as cur: cur.execute(""" UPDATE sync_log SET status = 'failed', completed_at = NOW(), error_message = 'Interrupted by container restart' WHERE status = 'running' AND completed_at IS NULL """) if cur.rowcount: logging.getLogger(__name__).info(f"Cleared {cur.rowcount} stale sync_log entries") finally: conn.close() except Exception as e: logging.getLogger(__name__).warning(f"Stale sync_log cleanup failed: {e}") # Ensure ai_insights table exists try: from database import SyncSessionLocal db = SyncSessionLocal() try: db.execute(text(""" CREATE TABLE IF NOT EXISTS ai_insights ( id SERIAL PRIMARY KEY, generated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), insight_type VARCHAR(50) NOT NULL DEFAULT 'daily_summary', content TEXT NOT NULL, model VARCHAR(100), input_tokens INTEGER, output_tokens INTEGER, data_snapshot JSONB, triggered_by VARCHAR(50) DEFAULT 'scheduler' ) """)) db.execute(text(""" CREATE INDEX IF NOT EXISTS idx_ai_insights_generated ON ai_insights(generated_at DESC) """)) db.commit() finally: db.close() except Exception as e: logging.getLogger(__name__).warning(f"AI insights table creation failed: {e}") start_scheduler() yield shutdown_scheduler() app = FastAPI( title="Forecasting API", description="Hotel & Restaurant Forecasting Service", version="2.0.0", lifespan=lifespan, ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # All routes — nginx strips /forecasting/api/ prefix before reaching here app.include_router(forecast.router, prefix="/forecast", tags=["Forecasts"]) app.include_router(sync.router, prefix="/sync", tags=["Data Sync"]) app.include_router(export.router, prefix="/export", tags=["Exports"]) app.include_router(budget.router, prefix="/budget", tags=["Budgets"]) app.include_router(accuracy.router, prefix="/accuracy", tags=["Accuracy"]) app.include_router(evolution.router, prefix="/evolution", tags=["Forecast Evolution"]) app.include_router(crossref.router, prefix="/crossref", tags=["Cross-Reference"]) app.include_router(explain.router, prefix="/explain", tags=["Explainability"]) app.include_router(config.router, prefix="/config", tags=["Configuration"]) app.include_router(historical.router, prefix="/historical", tags=["Historical Data"]) app.include_router(resos.router, prefix="/resos", tags=["Resos Mapping"]) app.include_router(backtest.router, prefix="/backtest", tags=["Backtesting"]) app.include_router(sync_bookings.router, prefix="/sync", tags=["Data Sync"]) app.include_router(resos_sync.router, prefix="/sync", tags=["Data Sync"]) app.include_router(reports.router, prefix="/reports", tags=["Reports"]) app.include_router(special_dates.router, prefix="/settings", tags=["Settings"]) app.include_router(backup.router, prefix="/backup", tags=["Backup & Restore"]) app.include_router(public.router, prefix="/public", tags=["Public API"]) app.include_router(bookability.router, prefix="/bookability", tags=["Bookability"]) app.include_router(competitor_rates.router, prefix="/competitor-rates", tags=["Competitor Rates"]) app.include_router(ai_insights.router, prefix="/ai-insights", tags=["AI Insights"]) @app.get("/health") async def health_check(): return {"status": "healthy", "service": "forecasting-api"}