feat: bump client-services (accumulated feature work + deploy fixes)

Snapshot of in-progress work across local_backend, manager_dashboard,
and waiter_pwa (pricing, chat, fiscal, prep zones, recovery codes, CRM,
inventory, permissions), plus the nginx/docker-compose deploy fixes for
the Unraid + NPM reverse-proxy setup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 10:00:14 +03:00
parent 02ec1aa28f
commit 34ae328b0d
182 changed files with 34874 additions and 3556 deletions

View File

@@ -0,0 +1,64 @@
"""
Phone call event receiver + WebSocket broadcaster.
The Grandstream UCM sends a simple HTTP GET to /api/phone/call-event
with caller ID and extension details. We broadcast that to all connected
WebSocket clients (manager dashboard Phone page).
The call-event endpoint is intentionally unauthenticated so the UCM device
(which has no JWT) can POST to it directly on the local LAN.
"""
import asyncio
import json
import logging
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query
router = APIRouter()
logger = logging.getLogger(__name__)
# In-memory list of active WebSocket connections to the Phone page.
_connections: list[WebSocket] = []
@router.websocket("/ws")
async def phone_ws(websocket: WebSocket):
await websocket.accept()
_connections.append(websocket)
logger.debug("phone_ws: client connected (%d total)", len(_connections))
try:
# Keep the connection alive; client drives nothing, server pushes events.
while True:
await asyncio.sleep(25)
await websocket.send_text(json.dumps({"type": "ping"}))
except (WebSocketDisconnect, Exception):
pass
finally:
_connections.remove(websocket)
logger.debug("phone_ws: client disconnected (%d total)", len(_connections))
@router.get("/call-event")
async def call_event(
caller: str = Query(..., description="Caller ID number from UCM"),
ext: str = Query("", description="Dialled extension"),
):
"""
Called by the Grandstream UCM Action URL on every incoming call.
Example: GET /api/phone/call-event?caller=2106001234&ext=100
"""
payload = json.dumps({"type": "incoming_call", "caller": caller, "ext": ext})
dead = []
for ws in list(_connections):
try:
await ws.send_text(payload)
except Exception:
dead.append(ws)
for ws in dead:
try:
_connections.remove(ws)
except ValueError:
pass
logger.info("phone call-event: caller=%s ext=%s (notified %d clients)", caller, ext, len(_connections) - len(dead))
return {"status": "ok", "notified": len(_connections) - len(dead)}