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:
mjalmoudy94
2026-10-13 12:56:36 +03:30
parent afdef4b444
commit f1e5df29ce
31 changed files with 2981 additions and 28 deletions
@@ -14,6 +14,7 @@ from __future__ import annotations
from datetime import datetime
from app.application.ports import PaymentProviderPort
from app.application.ports.realtime import emit_wallet_changed
from app.core.errors import NotFoundError, PermissionDeniedError, ValidationError
from app.domain.billing.models import (
CreditWallet,
@@ -121,6 +122,11 @@ class BillingService:
# Refuse to leave a wallet in debt; undo and surface the conflict.
await self.wallets.adjust(organization_id=organization_id, delta=-amount)
raise ValidationError("اعتبار کافی برای این کسر وجود ندارد.")
# Live push: the sidebar credit badge and the billing page follow the
# wallet as it moves, whoever moved it.
await emit_wallet_changed(
organization_id=organization_id, balance=wallet.balance
)
return wallet
async def start_checkout(
@@ -177,8 +183,13 @@ class BillingService:
return payment
if succeeded:
await self.wallets.adjust(organization_id=organization_id, delta=payment.amount)
wallet = await self.wallets.adjust(
organization_id=organization_id, delta=payment.amount
)
final = await self.payments.set_status(payment_id, PaymentStatus.SUCCEEDED)
await emit_wallet_changed(
organization_id=organization_id, balance=wallet.balance
)
else:
final = await self.payments.set_status(payment_id, PaymentStatus.FAILED)
return final or payment