📄 main.py 8,233 bytes Apr 29, 2026 📋 Raw

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