daily-briefing/server/main.py

173 lines
5.8 KiB
Python
Raw Normal View History

"""Daily Briefing Dashboard — FastAPI Application."""
from __future__ import annotations
import logging
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from server.config import get_settings, reload_settings, settings
from server.services.mqtt_service import mqtt_service
logger = logging.getLogger("daily-briefing")
logging.basicConfig(
level=logging.DEBUG if settings.debug else logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
# Reduce noise from third-party libraries — our services log their own summaries
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Startup / shutdown lifecycle."""
from server import db
from server.migrations.runner import run_migrations
from server.services.seed_service import seed_if_empty
logger.info("Starting Daily Briefing Dashboard v2.1...")
# 1. Initialize shared database pool (bootstrap from ENV)
try:
pool = await db.init_pool(
host=settings.db_host,
port=settings.db_port,
dbname=settings.db_name,
user=settings.db_user,
password=settings.db_password,
)
logger.info("Database pool initialized")
except Exception:
logger.exception("Failed to initialize database pool — admin + news will be unavailable")
yield
return
# 2. Run database migrations
try:
await run_migrations(pool)
except Exception:
logger.exception("Migration error — some features may not work")
# 3. Seed database from ENV on first run
try:
await seed_if_empty()
except Exception:
logger.exception("Seeding error — admin panel may need manual setup")
# 4. Load settings from database (overrides ENV defaults)
try:
await reload_settings()
cfg = get_settings()
# Structured startup summary
integrations = [
("Weather", True, f"{cfg.weather_location} + {cfg.weather_location_secondary}"),
("HA", cfg.ha_enabled, cfg.ha_url or "not configured"),
("Vikunja", cfg.vikunja_enabled, cfg.vikunja_url or "not configured"),
("Unraid", cfg.unraid_enabled, f"{len(cfg.unraid_servers)} server(s)"),
("MQTT", cfg.mqtt_enabled, f"{cfg.mqtt_host}:{cfg.mqtt_port}" if cfg.mqtt_host else "not configured"),
("News", cfg.news_enabled, f"max_age={cfg.news_max_age_hours}h"),
]
logger.info("--- Integration Status ---")
for name, enabled, detail in integrations:
status = "ON " if enabled else "OFF"
logger.info(" [%s] %-10s %s", status, name, detail)
logger.info("--------------------------")
except Exception:
logger.exception("Failed to load settings from DB — using ENV defaults")
cfg = settings
# 5. Start MQTT service if enabled
if cfg.mqtt_enabled and cfg.mqtt_host:
try:
await mqtt_service.start(
host=cfg.mqtt_host,
port=cfg.mqtt_port,
username=cfg.mqtt_username or None,
password=cfg.mqtt_password or None,
topics=cfg.mqtt_topics,
client_id=cfg.mqtt_client_id,
)
logger.info("MQTT service started (%s:%d)", cfg.mqtt_host, cfg.mqtt_port)
except Exception:
logger.exception("Failed to start MQTT service")
else:
logger.info("MQTT disabled — configure via Admin Panel or MQTT_HOST env")
yield
# Shutdown
logger.info("Shutting down...")
await mqtt_service.stop()
await db.close_pool()
app = FastAPI(
title="Daily Briefing",
version="2.1.0",
lifespan=lifespan,
)
# CORS — allow frontend dev server
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# --- Register Routers ---
from server.routers import admin, auth, dashboard, homeassistant, mqtt, news, servers, setup, tasks, weather # noqa: E402
app.include_router(setup.router)
app.include_router(auth.router)
app.include_router(admin.router)
app.include_router(weather.router)
app.include_router(news.router)
app.include_router(servers.router)
app.include_router(homeassistant.router)
app.include_router(tasks.router)
app.include_router(mqtt.router)
app.include_router(dashboard.router)
# --- Serve static frontend (production) ---
static_dir = Path(__file__).parent.parent / "static"
if static_dir.is_dir():
# SPA fallback: serve index.html for any non-API path
@app.get("/setup{full_path:path}")
async def setup_spa_fallback(full_path: str = ""):
index = static_dir / "index.html"
if index.exists():
return FileResponse(str(index))
return {"error": "Frontend not built"}
@app.get("/admin/{full_path:path}")
async def admin_spa_fallback(full_path: str):
index = static_dir / "index.html"
if index.exists():
return FileResponse(str(index))
return {"error": "Frontend not built"}
app.mount("/", StaticFiles(directory=str(static_dir), html=True), name="static")
logger.info("Serving static frontend from %s", static_dir)
else:
@app.get("/")
async def root():
return {
"status": "ok",
"message": "Daily Briefing API v2.1 — Frontend not built yet",
"endpoints": [
"/api/all", "/api/weather", "/api/news", "/api/servers",
"/api/ha", "/api/tasks", "/api/mqtt",
"/api/auth/login", "/api/admin/integrations",
],
}