59b125d8e2
- Add all_workspaces_router with GET /workspaces/ (no project/repo required) - Include project_id in workspace responses - Frontend: useWorkspaces() calls listAllWorkspaces when no args - Frontend: WorkspacesPage uses top-level list, derives project/repo from workspace for mutations - Fixes 422 from invalid UUID path params
166 lines
5.6 KiB
Python
166 lines
5.6 KiB
Python
import logging
|
||
import os
|
||
|
||
from fastapi import FastAPI, Request
|
||
from fastapi.exceptions import RequestValidationError
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from fastapi.responses import JSONResponse
|
||
from fastapi.staticfiles import StaticFiles
|
||
|
||
from src.api.auth import router as auth_router
|
||
from src.api.dashboard import router as dashboard_router
|
||
from src.api.events import router as events_router
|
||
from src.api.git_repositories import router as git_repositories_router
|
||
from src.api.health import router as health_router
|
||
from src.api.projects import router as projects_router
|
||
from src.api.ssh_keys import router as ssh_keys_router
|
||
from src.api.terminal import router as terminal_router
|
||
from src.api.instance_proxy import router as instance_proxy_router
|
||
from src.api.config_profiles import router as config_profiles_router
|
||
from src.api.tool_definitions import router as tool_definitions_router
|
||
from src.api.tool_instances import router as tool_instances_router
|
||
from src.api.tool_instances import sessions_router
|
||
from src.api.tool_types import router as tool_types_router
|
||
from src.api.notifications import router as notifications_router
|
||
from src.api.user_config import router as user_config_router
|
||
from src.api.users import router as users_router
|
||
from src.api.workspaces import all_workspaces_router, router as workspaces_router
|
||
from src.config import Settings
|
||
from src.models.notification import Notification # noqa: F401 – Alembic model discovery
|
||
from src.models.terminal_session import TerminalSessionModel # noqa: F401 – Alembic model discovery
|
||
from src.database import init_database
|
||
from src.logging_config import (
|
||
ExceptionLoggingMiddleware,
|
||
RequestLoggingMiddleware,
|
||
configure_logging,
|
||
)
|
||
from src.services.correlation import CorrelationIdMiddleware
|
||
from src.services.event_bus import InstanceEventBus
|
||
from src.services.health_monitor import HealthMonitor
|
||
|
||
# Configure logging early
|
||
log_level = os.getenv("LOG_LEVEL", "INFO").upper()
|
||
configure_logging(level=getattr(logging, log_level, logging.INFO))
|
||
|
||
logger = logging.getLogger(__name__)
|
||
settings = Settings()
|
||
app = FastAPI(title="Headquarter API")
|
||
|
||
# Configure CORS - must be before other middleware
|
||
# Build allowed origins list including web and api domains
|
||
cors_origins = [settings.web_base_url]
|
||
if settings.api_base_url != settings.web_base_url:
|
||
cors_origins.append(settings.api_base_url)
|
||
logger.info("CORS configured with origins: %s", cors_origins)
|
||
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=cors_origins,
|
||
allow_credentials=True,
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
app.add_middleware(CorrelationIdMiddleware)
|
||
app.add_middleware(RequestLoggingMiddleware)
|
||
app.add_middleware(ExceptionLoggingMiddleware)
|
||
|
||
|
||
def _sanitize_validation_errors(errors):
|
||
"""Convert validation errors to JSON-safe format."""
|
||
sanitized = []
|
||
for error in errors:
|
||
safe_error = {
|
||
"type": error.get("type"),
|
||
"loc": error.get("loc"),
|
||
"msg": error.get("msg"),
|
||
"input": str(error.get("input"))
|
||
if error.get("input") is not None
|
||
else None,
|
||
}
|
||
# Convert ctx to safe format
|
||
ctx = error.get("ctx")
|
||
if ctx:
|
||
safe_ctx = {}
|
||
for key, value in ctx.items():
|
||
if isinstance(value, Exception):
|
||
safe_ctx[key] = str(value)
|
||
elif isinstance(value, (str, int, float, bool, type(None))):
|
||
safe_ctx[key] = value
|
||
else:
|
||
safe_ctx[key] = str(value)
|
||
safe_error["ctx"] = safe_ctx
|
||
sanitized.append(safe_error)
|
||
return sanitized
|
||
|
||
|
||
@app.exception_handler(RequestValidationError)
|
||
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||
"""Log validation errors and return detailed response."""
|
||
errors = exc.errors()
|
||
logger.warning(
|
||
"Validation error for %s %s: %s",
|
||
request.method,
|
||
request.url.path,
|
||
errors,
|
||
)
|
||
safe_errors = _sanitize_validation_errors(errors)
|
||
return JSONResponse(
|
||
status_code=422,
|
||
content={"detail": safe_errors},
|
||
)
|
||
|
||
|
||
# Global services
|
||
_event_bus = InstanceEventBus()
|
||
_health_monitor = HealthMonitor(_event_bus)
|
||
|
||
|
||
@app.on_event("startup")
|
||
async def on_startup():
|
||
logger.info("Starting up Headquarter API...")
|
||
|
||
# Initialize database (run migrations)
|
||
db_ready = await init_database()
|
||
if not db_ready:
|
||
logger.error("Database initialization failed. Shutting down.")
|
||
import sys
|
||
|
||
sys.exit(1)
|
||
|
||
# Start background health monitor
|
||
_health_monitor.start()
|
||
logger.info("Health monitor started")
|
||
|
||
logger.info("Startup complete.")
|
||
|
||
|
||
@app.on_event("shutdown")
|
||
async def on_shutdown():
|
||
logger.info("Shutting down Headquarter API...")
|
||
_health_monitor.stop()
|
||
logger.info("Health monitor stopped")
|
||
logger.info("Shutdown complete.")
|
||
|
||
|
||
app.include_router(health_router)
|
||
app.include_router(auth_router)
|
||
app.include_router(dashboard_router)
|
||
app.include_router(projects_router)
|
||
app.include_router(users_router)
|
||
app.include_router(ssh_keys_router)
|
||
app.include_router(git_repositories_router)
|
||
app.include_router(user_config_router)
|
||
app.include_router(tool_types_router)
|
||
app.include_router(tool_definitions_router)
|
||
app.include_router(config_profiles_router)
|
||
app.include_router(tool_instances_router)
|
||
app.include_router(sessions_router)
|
||
app.include_router(instance_proxy_router)
|
||
app.include_router(terminal_router)
|
||
app.include_router(events_router)
|
||
app.include_router(notifications_router)
|
||
app.include_router(all_workspaces_router)
|
||
app.include_router(workspaces_router)
|
||
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
|