Complete admin backend with login, where all integrations (weather, news, Home Assistant, Vikunja, Unraid, MQTT) can be configured via web UI instead of ENV variables. Two-layer config: ENV seeds DB on first start, then DB is source of truth. Auto-migration system on startup. Backend: db.py shared pool, auth.py JWT, settings_service CRUD, seed_service, admin router (protected), test_connections per integration, config.py rewrite. Frontend: react-router v6, login page, admin layout with sidebar, 8 settings pages (General, Weather, News, HA, Vikunja, Unraid, MQTT, ChangePassword), shared IntegrationForm + TestButton components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
151 lines
4.7 KiB
Python
151 lines
4.7 KiB
Python
"""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",
|
|
)
|
|
|
|
|
|
@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()
|
|
logger.info(
|
|
"Settings loaded from DB — %d Unraid servers, MQTT=%s, HA=%s",
|
|
len(cfg.unraid_servers),
|
|
"enabled" if cfg.mqtt_enabled else "disabled",
|
|
"enabled" if cfg.ha_enabled else "disabled",
|
|
)
|
|
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, tasks, weather # noqa: E402
|
|
|
|
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("/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",
|
|
],
|
|
}
|