Files
wild-pc/dashboard-api/src/dashboard_api/stream.py
Paul Payne f39a551aad 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.
2026-02-20 16:41:19 -08:00

62 lines
1.8 KiB
Python

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