"""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)