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:
64
local_backend/routers/phone.py
Normal file
64
local_backend/routers/phone.py
Normal 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)}
|
||||
Reference in New Issue
Block a user