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:
commit
75d2c1fa9d
103 changed files with 70316 additions and 0 deletions
138
backend/main.py
Normal file
138
backend/main.py
Normal 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"}
|
||||
Loading…
Add table
Add a link
Reference in a new issue