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
129 lines
3.4 KiB
Python
129 lines
3.4 KiB
Python
"""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()
|