"""HoffDesk API - Host-based subdomain routing. Phase 1: Restructure as sub-applications for domain separation. - family.hoffdesk.com → Family dashboard + automation - notes.hoffdesk.com → Content generation + blog admin - hoffdesk.com → Public blog Usage: uvicorn main_v2:application --host 0.0.0.0 --port 8000 """ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from pathlib import Path from starlette.applications import Starlette from starlette.routing import Host, Mount from starlette.staticfiles import StaticFiles # Load .env file if python-dotenv is available try: from dotenv import load_dotenv env_path = Path(__file__).parent / ".env" if env_path.exists(): load_dotenv(env_path) except ImportError: pass from starlette.middleware.base import BaseHTTPMiddleware from shared.session_auth import SessionAuthMiddleware # Import all routers from blog import blog_router, brief_router from blog.admin_router import admin_router, setup_admin_static from content import router as content_router from family import family_router from webhook_public import webhook_router from auth.router import auth_router from dashboard.router import router as dashboard_router from dashboard.ui_router import router as ui_router # ============================================================================= # FAMILY APP (family.hoffdesk.com) # Dashboard + family automation # ============================================================================= family_app = FastAPI( title="Family Dashboard", description="HoffDesk family dashboard and automation", version="1.0.0" ) # Middleware family_app.add_middleware(SessionAuthMiddleware) family_app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Static files DASHBOARD_STATIC_DIR = Path("/home/hoffmann_admin/.openclaw/shared/project-docs/dashboard/static") family_app.mount("/static", StaticFiles(directory=str(DASHBOARD_STATIC_DIR)), name="family_static") # Auth router (mounted at /auth for consistency) family_app.include_router(auth_router, prefix="/auth", tags=["auth"]) # Dashboard (main page) family_app.include_router(dashboard_router, tags=["dashboard"]) # UI fragments (HTMX endpoints per CONTRACT.md) family_app.include_router(ui_router, tags=["ui"]) # Family automation endpoints family_app.include_router(family_router, prefix="/admin", tags=["family-automation"]) # Public webhooks (also available on family domain) family_app.include_router(webhook_router, tags=["webhook"]) # Root handler for family - dashboard (requires auth, handled by dashboard_router) # The dashboard_router handles / and /dashboard/ already # ============================================================================= # NOTES APP (notes.hoffdesk.com) # Content generation + blog admin # ============================================================================= notes_app = FastAPI( title="Notes & Content", description="HoffDesk content generation and blog administration", version="1.0.0" ) # Middleware notes_app.add_middleware(SessionAuthMiddleware) notes_app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Static files (shared with blog for now) BLOG_STATIC_DIR = Path("/home/hoffmann_admin/hoffdesk/blog/static") notes_app.mount("/static", StaticFiles(directory=str(BLOG_STATIC_DIR)), name="notes_static") # Auth router (mounted at /auth for consistency) notes_app.include_router(auth_router, prefix="/auth", tags=["auth"]) # Content generation notes_app.include_router(content_router, prefix="/admin", tags=["content-generation"]) # Blog admin notes_app.include_router(admin_router, prefix="/admin/blog", tags=["blog-admin"]) # Content briefs notes_app.include_router(brief_router, prefix="/api/v1/content/briefs", tags=["content-briefs"]) # Blog public API (also available on notes) notes_app.include_router(blog_router, prefix="/api/blog", tags=["blog"]) # Setup admin static files setup_admin_static(notes_app) # Root handler for notes - redirect to blog admin @notes_app.get("/") def notes_root(): from fastapi.responses import RedirectResponse return RedirectResponse(url="/admin/blog/", status_code=302) # ============================================================================= # BLOG APP (hoffdesk.com, www.hoffdesk.com) # Public blog only # ============================================================================= blog_app = FastAPI( title="HoffDesk Blog", description="Public HoffDesk blog", version="1.0.0" ) # CORS (no session auth for public blog) blog_app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Static files blog_app.mount("/blog/static", StaticFiles(directory=str(BLOG_STATIC_DIR)), name="blog_static") # Public blog API blog_app.include_router(blog_router, prefix="/api/blog", tags=["blog"]) # Public webhooks blog_app.include_router(webhook_router, tags=["webhook"]) # Health check @blog_app.get("/health") def health(): return {"status": "healthy", "app": "blog"} # Root redirect @blog_app.get("/") def root(): from fastapi.responses import RedirectResponse return RedirectResponse(url="/api/blog/", status_code=302) # ============================================================================= # MAIN HOST ROUTER # Routes by Host header to appropriate sub-application # ============================================================================= routes = [ Host("family.hoffdesk.com", app=family_app, name="family"), Host("notes.hoffdesk.com", app=notes_app, name="notes"), Host("hoffdesk.com", app=blog_app, name="blog"), Host("www.hoffdesk.com", app=blog_app, name="blog_www"), # Fallback: any other host goes to blog Mount("/", app=blog_app), ] # Create the host-routing application application = Starlette(routes=routes) # ============================================================================= # DEVELOPMENT MODE # For local testing without domain names # ============================================================================= dev_app = FastAPI( title="HoffDesk API (Dev Mode)", description="All routes mounted at paths for local development", version="1.0.0-dev" ) # Session auth dev_app.add_middleware(SessionAuthMiddleware) dev_app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Mount all static files dev_app.mount("/blog/static", StaticFiles(directory=str(BLOG_STATIC_DIR)), name="blog_static_dev") dev_app.mount("/static", StaticFiles(directory=str(DASHBOARD_STATIC_DIR)), name="dashboard_static_dev") # Mount all routers for local testing dev_app.include_router(auth_router, prefix="/auth", tags=["auth"]) dev_app.include_router(dashboard_router, prefix="/family", tags=["dashboard"]) dev_app.include_router(ui_router, prefix="/family/ui", tags=["ui"]) dev_app.include_router(family_router, prefix="/admin", tags=["family-automation"]) dev_app.include_router(content_router, prefix="/notes/admin", tags=["content-generation"]) dev_app.include_router(admin_router, prefix="/notes/admin/blog", tags=["blog-admin"]) dev_app.include_router(brief_router, prefix="/api/v1/content/briefs", tags=["content-briefs"]) dev_app.include_router(blog_router, prefix="/api/blog", tags=["blog"]) dev_app.include_router(webhook_router, tags=["webhook"]) setup_admin_static(dev_app) @dev_app.get("/") def dev_root(): from fastapi.responses import RedirectResponse return RedirectResponse(url="/api/blog/", status_code=302) @dev_app.get("/health") def dev_health(): return {"status": "healthy", "mode": "development"} # Export based on environment import os if os.getenv("HOST_ROUTING", "true").lower() == "true": # Production: use host-based routing app = application else: # Development: use path-based routing app = dev_app if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)