import asyncio import logging import os from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from models.schemas import HealthCheck from routers import drives, enclosures, leds, overview, temps from services import store from services.cache import close_cache, init_cache, redis_available from services.mqtt_publisher import MqttPublisher, _PAHO_AVAILABLE, mqtt_enabled logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", ) logger = logging.getLogger(__name__) _tool_status: dict[str, bool] = {} _mqtt_task: asyncio.Task | None = None _mqtt_publisher: MqttPublisher | None = None @asynccontextmanager async def lifespan(app: FastAPI): """API/web + MQTT consumer. Reads everything from Redis; the dedicated poller process is the only thing that touches hardware.""" global _mqtt_task, _mqtt_publisher await init_cache() _tool_status["redis"] = redis_available() # Optional MQTT publisher for Home Assistant (opt-in via MQTT_HOST). _tool_status["mqtt"] = False if mqtt_enabled(): if not _PAHO_AVAILABLE: logger.warning("MQTT_HOST set but paho-mqtt not installed — MQTT disabled") else: try: _mqtt_publisher = MqttPublisher() _mqtt_publisher.start() _mqtt_task = asyncio.create_task(_mqtt_publisher.run()) _tool_status["mqtt"] = True except Exception as e: logger.error("Failed to start MQTT publisher: %s", e) yield if _mqtt_task is not None: _mqtt_task.cancel() try: await _mqtt_task except asyncio.CancelledError: pass if _mqtt_publisher is not None: _mqtt_publisher.stop() await close_cache() app = FastAPI( title="JBOD Monitor", description="Drive health monitoring for JBOD enclosures", version="0.2.0", lifespan=lifespan, ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) app.include_router(enclosures.router) app.include_router(drives.router) app.include_router(leds.router) app.include_router(overview.router) app.include_router(temps.router) @app.get("/api/health", response_model=HealthCheck, tags=["health"]) async def health(): tools = dict(_tool_status) tools["redis"] = redis_available() # Poller freshness: is the producer keeping the store warm? meta = await store.get_meta() if meta and meta.get("last_sweep_ts"): age = store.now() - meta["last_sweep_ts"] tools["poller_fresh"] = age < meta.get("sweep_interval", 300) * 3 else: tools["poller_fresh"] = False return HealthCheck(status="ok", tools=tools) # Serve built frontend static files — mounted last so /api routes take priority. STATIC_DIR = Path(__file__).parent / "static" if STATIC_DIR.exists(): app.mount("/", StaticFiles(directory=STATIC_DIR, html=True), name="static")