Python FastAPI ML backend kept intact; auth replaced with central hnf_session cookie verification. Frontend rebuilt on React 18 + TS + Vite with stack design system, Plotly charts retained. Shared Postgres via DATABASE_URL; schema applied on startup. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
138 lines
5.6 KiB
Python
138 lines
5.6 KiB
Python
"""
|
|
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)
|
|
try:
|
|
import os
|
|
from database import SyncSessionLocal
|
|
schema_path = os.path.join(os.path.dirname(__file__), 'schema.sql')
|
|
if os.path.exists(schema_path):
|
|
db = SyncSessionLocal()
|
|
try:
|
|
with open(schema_path) as f:
|
|
sql = f.read()
|
|
db.execute(text(sql))
|
|
db.commit()
|
|
logging.getLogger(__name__).info("Schema applied successfully")
|
|
except Exception as e:
|
|
logging.getLogger(__name__).warning(f"Schema init failed: {e}")
|
|
db.rollback()
|
|
finally:
|
|
db.close()
|
|
except Exception as e:
|
|
logging.getLogger(__name__).warning(f"Schema load 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}")
|
|
|
|
# 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"}
|