"""UI main entry point.""" import logging import os from pathlib import Path from fastapi import FastAPI, Request from fastapi.responses import HTMLResponse, JSONResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from apps.ui.api_client import fetch_devices, fetch_layout logger = logging.getLogger(__name__) # Read configuration from environment variables API_BASE = os.getenv("API_BASE", "http://localhost:8001") BASE_PATH = os.getenv("BASE_PATH", "") # e.g., "/ui" for reverse proxy print(f"UI using API_BASE: {API_BASE}") print(f"UI using BASE_PATH: {BASE_PATH}") def api_url(path: str) -> str: """Helper function to construct API URLs. Args: path: API path (e.g., "/devices") Returns: Full API URL """ return f"{API_BASE}{path}" # Initialize FastAPI app with optional root_path for reverse proxy app = FastAPI( title="Home Automation UI", description="User interface for home automation system", version="0.1.0", root_path=BASE_PATH ) # Setup Jinja2 templates templates_dir = Path(__file__).parent / "templates" templates = Jinja2Templates(directory=str(templates_dir)) # Setup static files static_dir = Path(__file__).parent / "static" static_dir.mkdir(exist_ok=True) app.mount("/static", StaticFiles(directory=str(static_dir)), name="static") @app.get("/health") async def health() -> JSONResponse: """Health check endpoint for Kubernetes/Docker. Returns: JSONResponse: Health status """ return JSONResponse({ "status": "ok", "service": "ui", "api_base": API_BASE, "base_path": BASE_PATH }) @app.get("/", response_class=HTMLResponse) async def index(request: Request) -> HTMLResponse: """Redirect to dashboard. Args: request: The FastAPI request object Returns: HTMLResponse: Rendered dashboard """ return await rooms(request) @app.get("/rooms", response_class=HTMLResponse) async def rooms(request: Request) -> HTMLResponse: """Render the rooms overview page. Args: request: The FastAPI request object Returns: HTMLResponse: Rendered rooms template """ return templates.TemplateResponse("rooms.html", { "request": request, "api_base": API_BASE }) @app.get("/room/{room_name}", response_class=HTMLResponse) async def room_detail(request: Request, room_name: str) -> HTMLResponse: """Render the room detail page with devices. Args: request: The FastAPI request object room_name: Name of the room to display Returns: HTMLResponse: Rendered room template """ return templates.TemplateResponse("room.html", { "request": request, "api_base": API_BASE, "room_name": room_name }) @app.get("/device/{device_id}", response_class=HTMLResponse) async def device_detail(request: Request, device_id: str) -> HTMLResponse: """Render the device detail page with controls. Args: request: The FastAPI request object device_id: ID of the device to display Returns: HTMLResponse: Rendered device template """ return templates.TemplateResponse("device.html", { "request": request, "api_base": API_BASE, "device_id": device_id }) @app.get("/garage", response_class=HTMLResponse) async def garage(request: Request) -> HTMLResponse: """Render the garage page with car outlet devices. Args: request: The FastAPI request object Returns: HTMLResponse: Rendered garage template """ return templates.TemplateResponse("garage.html", { "request": request, "api_base": API_BASE }) @app.get("/dashboard", response_class=HTMLResponse) async def dashboard(request: Request) -> HTMLResponse: """Render the dashboard with rooms and devices. Args: request: The FastAPI request object Returns: HTMLResponse: Rendered dashboard template """ try: # Load layout from API (use configured API_BASE) layout_data = fetch_layout(API_BASE) # Fetch devices from API (now includes features) api_devices = fetch_devices(API_BASE) # Create device lookup by device_id device_map = {d["device_id"]: d for d in api_devices} # Build rooms with merged device data rooms = [] for room in layout_data.get("rooms", []): devices = [] for tile in room.get("devices", []): # Merge tile data with API device data device_data = { "device_id": tile["device_id"], "title": tile["title"], "icon": tile["icon"], "rank": tile["rank"], } # Add type, name, and features from API if available if tile["device_id"] in device_map: api_device = device_map[tile["device_id"]] device_data["type"] = api_device.get("type") device_data["name"] = api_device.get("name") device_data["features"] = api_device.get("features", {}) else: device_data["features"] = {} devices.append(device_data) # Sort devices by rank (ascending) devices.sort(key=lambda d: d["rank"]) rooms.append({ "name": room["name"], "devices": devices }) logger.info(f"Rendering dashboard with {len(rooms)} rooms") return templates.TemplateResponse("dashboard.html", { "request": request, "rooms": rooms, "api_base": API_BASE # Pass API_BASE to template }) except Exception as e: logger.error(f"Error rendering dashboard: {e}", exc_info=True) # Fallback to empty dashboard return templates.TemplateResponse("dashboard.html", { "request": request, "rooms": [], "api_base": API_BASE # Pass API_BASE even on error }) def main() -> None: """Run the UI application with uvicorn.""" import uvicorn uvicorn.run( "apps.ui.main:app", host="0.0.0.0", port=8002, reload=True ) if __name__ == "__main__": main()