feat: realtime WebSocket layer for live events and streaming logs
- Backend: ConnectionManager, Redis-backed bus with in-process fallback, channel authorization, and typed emit helpers wired into workers (ai_runtime, sandbox_gateway), workforce/sandbox/billing/audit/error services. - Frontend: single shared WS client with backoff + heartbeat, RealtimeProvider, useLiveChannel/useLiveEvent hooks, live indicators on task detail, manager chat, dashboard, traces, sandbox terminal, admin errors, and billing. - Contracts: WsEvent/WsServerFrame/WsClientFrame types mirroring the port. All 255 backend tests pass; web typecheck and build pass.
This commit is contained in:
@@ -125,6 +125,11 @@ def main(argv: list[str] | None = None) -> int:
|
||||
format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
|
||||
)
|
||||
args = _build_parser().parse_args(argv)
|
||||
# A worker is its own process: publishing here is how a run it starts, or a
|
||||
# session it reaps, reaches the browsers connected to the API process.
|
||||
from app.interfaces.realtime.bus import install as install_realtime
|
||||
|
||||
install_realtime()
|
||||
return asyncio.run(_main_async(args))
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ the service's business.
|
||||
from __future__ import annotations
|
||||
|
||||
from app.application.container import build_workforce_service
|
||||
from app.application.ports.realtime import emit_worker_status
|
||||
from app.core.errors import AppError
|
||||
from app.domain.audit.models import ErrorSeverity, ErrorSource
|
||||
from app.domain.workforce.models import TaskStatus
|
||||
@@ -87,6 +88,13 @@ class AiRuntimeWorker:
|
||||
|
||||
outcome.processed += 1
|
||||
outcome.note(f"task {task.id} -> run {run.id} ({run.status})")
|
||||
# This worker is its own process: without a push here, a run
|
||||
# started by the queue is invisible until someone reloads.
|
||||
await emit_worker_status(
|
||||
organization_id=task.organization_id,
|
||||
worker=self.name,
|
||||
message=f"کار «{current.title}» در حال اجراست (اجرا {run.id}).",
|
||||
)
|
||||
|
||||
return outcome
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
|
||||
from app.application.container import build_sandbox_service
|
||||
from app.application.ports.realtime import emit_sandbox_reaped
|
||||
from app.core.config import settings
|
||||
from app.domain.audit.models import AuditAction, AuditEvent
|
||||
from app.domain.base import utcnow
|
||||
@@ -72,6 +73,13 @@ class SandboxGatewayWorker:
|
||||
)
|
||||
outcome.processed += 1
|
||||
outcome.note(f"stopped session {sandbox_session.id}")
|
||||
# A reaped sandbox is a tenant-visible fact: push it so the
|
||||
# project page's terminal stops showing a live session.
|
||||
await emit_sandbox_reaped(
|
||||
organization_id=workspace.organization_id if workspace else None,
|
||||
session_id=sandbox_session.id,
|
||||
workspace_id=sandbox_session.workspace_id,
|
||||
)
|
||||
|
||||
if not stopped:
|
||||
outcome.skipped += 1
|
||||
|
||||
Reference in New Issue
Block a user