feat: add ComponentGrid and ComponentTable components for displaying components in grid and table formats
feat: implement HealthBadge and RoleBadge components for displaying health status and roles feat: create LogViewer component for streaming logs of services feat: develop SecretsEditor for managing secrets with CRUD operations feat: introduce ToolCard component for displaying tool information style: add global CSS variables and styles for consistent theming feat: set up API client for handling requests to the backend feat: implement hooks for fetching components, statuses, and tools feat: create routes for dashboard, component details, and tools feat: add service management endpoints for starting, stopping, and restarting services feat: implement event bus for handling real-time updates via SSE feat: create health check and logs endpoints for monitoring services feat: add tests for health and tools endpoints to ensure functionality chore: update project configuration files and dependencies for better development experience
This commit is contained in:
128
castle-api/src/castle_api/main.py
Normal file
128
castle-api/src/castle_api/main.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Castle API — dashboard data, health aggregation, event bus, service management."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from starlette.responses import StreamingResponse
|
||||
|
||||
from castle_api.bus import bus
|
||||
from castle_api.config import settings
|
||||
from castle_api.config_editor import router as config_router
|
||||
from castle_api.events import router as events_router
|
||||
from castle_api.logs import router as logs_router
|
||||
from castle_api.routes import router as dashboard_router
|
||||
from castle_api.secrets import router as secrets_router
|
||||
from castle_api.services import router as services_router
|
||||
from castle_api.stream import close_all_subscribers, health_poll_loop, subscribe, unsubscribe
|
||||
from castle_api.tools import router as tools_router
|
||||
|
||||
# Set by _watch_shutdown when uvicorn begins its shutdown sequence.
|
||||
_shutting_down = False
|
||||
|
||||
|
||||
async def _watch_shutdown(server: uvicorn.Server) -> None:
|
||||
"""Poll uvicorn's should_exit flag and close SSE subscribers promptly."""
|
||||
while not server.should_exit:
|
||||
await asyncio.sleep(0.5)
|
||||
global _shutting_down
|
||||
_shutting_down = True
|
||||
close_all_subscribers()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
"""Application lifespan handler."""
|
||||
global _shutting_down
|
||||
_shutting_down = False
|
||||
|
||||
await bus.start()
|
||||
poll_task = asyncio.create_task(health_poll_loop())
|
||||
|
||||
yield
|
||||
|
||||
_shutting_down = True
|
||||
poll_task.cancel()
|
||||
close_all_subscribers()
|
||||
await bus.stop()
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="castle-api",
|
||||
description="Castle API, event bus, and service management",
|
||||
version="0.1.0",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(config_router)
|
||||
app.include_router(dashboard_router)
|
||||
app.include_router(events_router)
|
||||
app.include_router(logs_router)
|
||||
app.include_router(secrets_router)
|
||||
app.include_router(services_router)
|
||||
app.include_router(tools_router)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health() -> dict[str, str]:
|
||||
"""Health check endpoint."""
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.get("/stream")
|
||||
async def sse_stream() -> StreamingResponse:
|
||||
"""SSE stream — pushes health updates and service action events."""
|
||||
q = subscribe()
|
||||
|
||||
async def event_generator() -> AsyncGenerator[str, None]:
|
||||
try:
|
||||
yield "event: connected\ndata: {}\n\n"
|
||||
while True:
|
||||
msg = await q.get()
|
||||
if not msg:
|
||||
break
|
||||
yield msg
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
unsubscribe(q)
|
||||
|
||||
return StreamingResponse(
|
||||
event_generator(),
|
||||
media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
||||
)
|
||||
|
||||
|
||||
def run() -> None:
|
||||
"""Run the application with uvicorn."""
|
||||
config = uvicorn.Config(
|
||||
"castle_api.main:app",
|
||||
host=settings.host,
|
||||
port=settings.port,
|
||||
reload=False,
|
||||
)
|
||||
server = uvicorn.Server(config)
|
||||
|
||||
async def serve_with_watcher() -> None:
|
||||
watcher = asyncio.create_task(_watch_shutdown(server))
|
||||
await server.serve()
|
||||
watcher.cancel()
|
||||
|
||||
asyncio.run(serve_with_watcher())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
Reference in New Issue
Block a user