feat: Add new tools and configurations for Castle platform
- Introduced `uv.lock` for dependency management with various packages including `pytest`, `colorama`, and `pluggy`. - Added `pyrightconfig.json` for Python type checking configuration. - Expanded `recommendations.md` with detailed scaling recommendations and project management strategies. - Created shared `ruff.toml` for consistent linting across projects. - Developed `Castle Tools` with various utilities including Android backup, browser automation, document conversion, and search tools. - Implemented `backup-collect` and `schedule` tools for system administration tasks. - Enhanced `search` functionality with indexing and querying capabilities using Tantivy. - Added comprehensive documentation for each tool, including usage examples and installation instructions.
This commit is contained in:
61
dashboard-api/src/dashboard_api/stream.py
Normal file
61
dashboard-api/src/dashboard_api/stream.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""SSE stream — pushes health updates and service action events to connected clients."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
|
||||
from castle_cli.config import load_config
|
||||
|
||||
from dashboard_api.config import settings
|
||||
from dashboard_api.health import check_all_health
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# All connected SSE clients receive events through this queue-based broadcast.
|
||||
_subscribers: list[asyncio.Queue[str]] = []
|
||||
|
||||
|
||||
def subscribe() -> asyncio.Queue[str]:
|
||||
"""Register a new SSE client. Returns a queue to read events from."""
|
||||
q: asyncio.Queue[str] = asyncio.Queue(maxsize=64)
|
||||
_subscribers.append(q)
|
||||
return q
|
||||
|
||||
|
||||
def unsubscribe(q: asyncio.Queue[str]) -> None:
|
||||
"""Remove a disconnected SSE client."""
|
||||
try:
|
||||
_subscribers.remove(q)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
async def broadcast(event_type: str, data: dict) -> None:
|
||||
"""Send an event to all connected SSE clients."""
|
||||
payload = f"event: {event_type}\ndata: {json.dumps(data)}\n\n"
|
||||
dead: list[asyncio.Queue[str]] = []
|
||||
for q in _subscribers:
|
||||
try:
|
||||
q.put_nowait(payload)
|
||||
except asyncio.QueueFull:
|
||||
dead.append(q)
|
||||
for q in dead:
|
||||
unsubscribe(q)
|
||||
|
||||
|
||||
async def health_poll_loop(interval: float = 10.0) -> None:
|
||||
"""Background task that polls health and broadcasts updates."""
|
||||
while True:
|
||||
try:
|
||||
config = load_config(settings.castle_root)
|
||||
statuses = await check_all_health(config)
|
||||
await broadcast("health", {
|
||||
"statuses": [s.model_dump() for s in statuses],
|
||||
"timestamp": time.time(),
|
||||
})
|
||||
except Exception:
|
||||
logger.exception("Health poll failed")
|
||||
await asyncio.sleep(interval)
|
||||
Reference in New Issue
Block a user