Forecasting app: hybrid port to HNF stack

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>
This commit is contained in:
jtricerolph 2026-07-04 18:49:34 +00:00
commit 75d2c1fa9d
103 changed files with 70316 additions and 0 deletions

138
backend/main.py Normal file
View file

@ -0,0 +1,138 @@
"""
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"}